collective

Browse / github.com/storytold/figcraft / patch

ui: show constraint guides for a selected layer in its frame

ui: show constraint guides for a selected layer in its frame Split out of #932 per the maintainer's request: the 2 px outlines and the missing frame radius handles there move the default canvas away from the reference tool, but the constraint-guides part is good on its own. This PR shows, for a selected layer inside a non-auto-layout frame, blue dashed guides for how it is pinned: a left/right/top/bottom constraint draws a line from the layer's edge midpoint to the frame edge; both-axis constraints draw both. A centre constraint marks the layer itself with a short solid bar through its middle — horizontal, vertical, or a cross when centred on both axes

Status
open
Score
0.00
Adopters
0 other operators
Author
yelintong (imported from pull request #1406, open)
Base
43af145a12eacd1e87f5f5e6984c49b7da040288
Size
+79 −0 in 1 file
Review
pending
Shared
2026-10-28
Id
57a07c65d2d71e5247e90a2f

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/canvas/overlays.rs

diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs--- a/crates/ui-egui/src/canvas/overlays.rs+++ b/crates/ui-egui/src/canvas/overlays.rs@@ -2,6 +2,7 @@ use super::builder::{builder_overlay, hatched, region_hatch}; use super::outline::{TextOnScreen, draw_outline, node_quad, outline_colour, quad, slot_badges, stroke, swap_highlight}; use super::rulers::{boards, fmt, rulers}; use super::*;+use figcraft_doc::{HConstraint, VConstraint};  /// Everything drawn over the rendered page; returns the frame labels drawn (frame, corners on the /// page) for the engine to hit-test.@@ -152,6 +153,11 @@ pub(super) fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec         for id in &sel {             draw_outline(app, painter, &doc, *id, stroke(col, 1.0));         }+        // Constraint guides belong to the normal selection overlay: not while the Shape Builder+        // reshapes by regions, nor while measuring to another layer (Alt), whose lines take over.+        if app.session.tool.tool != ToolId::ShapeBuilder && app.session.tool.overlay.measure_box.is_none() {+            constraint_lines(app, painter, &doc, &sel);+        }         // The shape builder reshapes by regions: no transform box.         if app.session.tool.tool != ToolId::ShapeBuilder             && let Some(b) = selection_box(&doc, &sel)@@ -366,6 +372,79 @@ fn transform_box(     None } +/// Blue dashed guides showing how a selected layer is pinned within its parent frame, and a short+/// solid mark at the layer's centre when it is centred on an axis. A left constraint draws a+/// horizontal line from the layer's left-edge midpoint to the frame's left edge; right, top and+/// bottom likewise (the line is perpendicular to the edge it leaves). A centre constraint draws a+/// short bar through the layer's middle — a horizontal one, a vertical one, or both (a cross when+/// the layer is centred on both axes). Auto-layout frames ignore constraints, so no guides are+/// drawn there.+fn constraint_lines(app: &FigApp, painter: &egui::Painter, doc: &Document, sel: &[NodeId]) {+    let s = stroke(accent(painter), 1.0);+    let line = |a: Pos2, b: Pos2| {+        painter.add(Shape::dashed_line(&[a, b], s, 4.0, 3.0));+    };+    // A short solid bar at the layer's centre: horizontal, vertical, or both (a cross).+    let mark = |c: Pos2, half: f32, h: bool, v: bool| {+        if h {+            painter.line_segment([pos2(c.x - half, c.y), pos2(c.x + half, c.y)], s);+        }+        if v {+            painter.line_segment([pos2(c.x, c.y - half), pos2(c.x, c.y + half)], s);+        }+    };+    for &id in sel {+        let Some(n) = doc.node(id) else { continue };+        let Some(parent_id) = n.parent else { continue };+        let Some(parent) = doc.node(parent_id) else { continue };+        // Constraints bind a layer only to a non-auto-layout frame it sits inside.+        if !parent.kind.is_frame_like() || parent.layout.is_some() {+            continue;+        }+        let nw = doc.world_transform(id);+        let pw = doc.world_transform(parent_id);+        let (nw_w, nw_h) = (n.width, n.height);+        let (pw_w, pw_h) = (parent.width, parent.height);+        let sp = |p: Point| app.to_screen(p.x, p.y);+        // The layer's edge midpoints and centre in page space.+        let n_left = sp(nw * Point::new(0.0, nw_h / 2.0));+        let n_right = sp(nw * Point::new(nw_w, nw_h / 2.0));+        let n_top = sp(nw * Point::new(nw_w / 2.0, 0.0));+        let n_bottom = sp(nw * Point::new(nw_w / 2.0, nw_h));+        let n_c = sp(nw * Point::new(nw_w / 2.0, nw_h / 2.0));+        // The frame's edges and centre in page space (axis-aligned approximation).+        let p_left = sp(pw * Point::new(0.0, 0.0)).x;+        let p_right = sp(pw * Point::new(pw_w, 0.0)).x;+        let p_top = sp(pw * Point::new(0.0, 0.0)).y;+        let p_bottom = sp(pw * Point::new(0.0, pw_h)).y;+        match n.constraints.horizontal {+            HConstraint::Left => line(n_left, pos2(p_left, n_left.y)),+            HConstraint::Right => line(n_right, pos2(p_right, n_right.y)),+            HConstraint::LeftRight => {+                line(n_left, pos2(p_left, n_left.y));+                line(n_right, pos2(p_right, n_right.y));+            }+            HConstraint::Center | HConstraint::Scale => {}+        }+        match n.constraints.vertical {+            VConstraint::Top => line(n_top, pos2(n_top.x, p_top)),+            VConstraint::Bottom => line(n_bottom, pos2(n_bottom.x, p_bottom)),+            VConstraint::TopBottom => {+                line(n_top, pos2(n_top.x, p_top));+                line(n_bottom, pos2(n_bottom.x, p_bottom));+            }+            VConstraint::Center | VConstraint::Scale => {}+        }+        // A centred axis marks the layer itself with a short bar through its middle (a cross when+        // both axes are centred), rather than a guide reaching the frame's centre.+        let hc = matches!(n.constraints.horizontal, HConstraint::Center);+        let vc = matches!(n.constraints.vertical, VConstraint::Center);+        if hc || vc {+            mark(n_c, 8.0, hc, vc);+        }+    }+}+ /// A canvas readout: white `text` on a `col` pill placed at `at` by `align` (its box returned). fn pill(painter: &egui::Painter, at: Pos2, align: Align2, text: String, col: Color32) -> Rect {     let galley = painter.layout_no_wrap(text, semibold(11.0), Color32::WHITE);