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
@@ -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);