🎉 Clip out outer board guide lines

This commit is contained in:
Belén Albeza 2026-06-11 15:02:52 +02:00
parent b82d04e172
commit c4d10db852
8 changed files with 113 additions and 25 deletions

View File

@ -551,11 +551,11 @@
;; Ruler guides: push the page guides to the render engine whenever they
;; change or their visibility toggles. When hidden we send an empty set.
;; While dragging, exclude the active guide so the SVG preview is the only line.
(mf/with-effect [@canvas-init? guides show-rulers? show-grids? @dragging-guide-id*]
(mf/with-effect [@canvas-init? guides objects show-rulers? show-grids? @dragging-guide-id*]
(when @canvas-init?
(let [guides (if (and show-rulers? show-grids?) (or guides {}) {})
guides (if-let [id @dragging-guide-id*] (dissoc guides id) guides)]
(wasm.api/set-guides guides))))
(wasm.api/set-guides guides objects))))
(hooks/setup-dom-events zoom disable-paste-ref in-viewport-ref read-only? drawing-tool path-drawing?)
(hooks/setup-viewport-size vport viewport-ref)

View File

@ -2244,13 +2244,14 @@
(defn set-guides
"Serializes the page guides and sends them to the render engine.
`guides` is the page `:guides` map (id -> guide)."
[guides]
`guides` is the page `:guides` map (id -> guide); `objects` is the page
objects map, used to resolve each guide's board clip range."
[guides objects]
(let [size (sr/get-guides-byte-size guides)
offset (mem/alloc->offset-32 size)
heapu32 (mem/get-heap-u32)
heapf32 (mem/get-heap-f32)]
(sr/write-guides guides heapu32 heapf32 offset)
(sr/write-guides guides objects heapu32 heapf32 offset)
(h/call wasm/internal-module "_set_guides")
(request-render "set-guides")))

View File

@ -8,7 +8,9 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.types.color :as clr]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.render-wasm.serializers.color :as sr-clr]
[app.render-wasm.wasm :as wasm]
@ -286,9 +288,11 @@
;; --- Guides
;; Each guide is serialized as 3 x 32-bit words:
;; kind (u32) | color (u32 argb) | position (f32)
(def ^:private guide-entry-size 12)
;; Each guide is serialized as 5 x 32-bit words:
;; kind (u32) | color (u32 argb) | position (f32) | frame-start (f32) | frame-end (f32)
;; `frame-start`/`frame-end` hold the board clip range (along the guide's line
;; direction); they are NaN when the guide is not bound to a board.
(def ^:private guide-entry-size 20)
;; Default guide color used when a guide has no explicit color (matches the
;; previous SVG overlay `default-guide-color`).
@ -311,19 +315,37 @@
[guides]
(+ 4 (* (count (or guides {})) guide-entry-size)))
(defn- guide-frame-range
"Returns the `[start end]` range of the board a guide belongs to, along the
guide's line direction (the board's y-range for vertical `:x` guides, its
x-range for horizontal `:y` guides). Returns nil for free guides and for
guides whose board is not a non-rotated root frame, matching the SVG
renderer's clip behavior."
[guide objects]
(when-let [frame (some->> (get guide :frame-id) (get objects))]
(when (and (cfh/root-frame? frame)
(not (ctst/rotated-frame? frame)))
(if (= :x (get guide :axis))
[(:y frame) (+ (:y frame) (:height frame))]
[(:x frame) (+ (:x frame) (:width frame))]))))
(defn write-guides
"Writes `guides` (a map id -> guide) into the heap views starting at the
32-bit `offset`. Layout: count header (u32) followed by
`kind | color | position` per guide."
[guides heapu32 heapf32 offset]
`kind | color | position | frame-start | frame-end` per guide. The frame
range is resolved from `objects` and written as NaN for free guides."
[guides objects heapu32 heapf32 offset]
(let [guides (vec (vals (or guides {})))
total (count guides)]
(aset heapu32 offset total)
(loop [i 0]
(when (< i total)
(let [guide (nth guides i)
base (+ offset 1 (* i 3))]
base (+ offset 1 (* i 5))
[frame-start frame-end] (guide-frame-range guide objects)]
(aset heapu32 base (translate-guide-axis (get guide :axis)))
(aset heapu32 (+ base 1) (sr-clr/hex->u32argb (or (get guide :color) default-guide-color) 1))
(aset heapf32 (+ base 2) (get guide :position)))
(aset heapf32 (+ base 2) (get guide :position))
(aset heapf32 (+ base 3) (or frame-start js/NaN))
(aset heapf32 (+ base 4) (or frame-end js/NaN)))
(recur (inc i))))))

View File

@ -67,7 +67,14 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
rulers::render(canvas, viewbox, &render_state.fonts, &ruler_state);
// TODO: pass guides data here
let (horizontal, vertical) = get_ui_state().guides();
guides::render(canvas, zoom, viewbox.area, horizontal, vertical);
guides::render(
canvas,
zoom,
render_state.options.dpr,
viewbox.area,
horizontal,
vertical,
);
canvas.restore();

View File

@ -8,30 +8,57 @@ use crate::ui::{Guide, GuideKind};
pub fn render(
canvas: &skia::Canvas,
zoom: f32,
dpr: f32,
area: Rect,
horizontal: &[Guide],
vertical: &[Guide],
) {
for guide in horizontal {
render_guide(canvas, zoom, area, *guide);
render_guide(canvas, zoom, dpr, area, *guide);
}
for guide in vertical {
render_guide(canvas, zoom, area, *guide);
render_guide(canvas, zoom, dpr, area, *guide);
}
}
pub fn render_guide(canvas: &skia::Canvas, zoom: f32, area: Rect, guide: Guide) {
pub fn render_guide(canvas: &skia::Canvas, zoom: f32, dpr: f32, area: Rect, guide: Guide) {
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(Into::<skia::Color>::into(guide.color));
// we disable antialias and increase the stroke thickness so the guides
// do not appear faint or blurry.
paint.set_alpha((0.7 * 255.0) as u8);
paint.set_stroke_width(1.0 * dpr / zoom);
// we disable antialias so the guides do not appear faint or blurry.
paint.set_anti_alias(false);
paint.set_stroke_width(2.0 / zoom);
// The guide line spans the whole viewport, but when it belongs to a board
// the solid part is clipped to that board's range (along the line
// direction). The trimmed-out parts are not drawn here; the hover/drag
// dashed decorations are rendered by the SVG overlay instead.
let (full_start, full_end) = match guide.kind {
GuideKind::Vertical(_) => (area.top, area.bottom),
GuideKind::Horizontal(_) => (area.left, area.right),
};
let (start, end) = match guide.frame_range {
Some((frame_start, frame_end)) => {
let (lo, hi) = if frame_start <= frame_end {
(frame_start, frame_end)
} else {
(frame_end, frame_start)
};
(lo.max(full_start), hi.min(full_end))
}
None => (full_start, full_end),
};
// The clipped range can fall entirely outside the viewport.
if start > end {
return;
}
let (x1, y1, x2, y2) = match guide.kind {
GuideKind::Vertical(x) => (x, area.top, x, area.bottom),
GuideKind::Horizontal(y) => (area.left, y, area.right, y),
GuideKind::Vertical(x) => (x, start, x, end),
GuideKind::Horizontal(y) => (start, y, end, y),
};
canvas.draw_line((x1, y1), (x2, y2), &paint);

View File

@ -109,11 +109,21 @@ mod tests {
use crate::shapes::Color;
fn vertical_guide(position: f32, index: usize) -> Guide {
Guide::new(GuideKind::Vertical(position), Color::BLACK, Some(index))
Guide::new(
GuideKind::Vertical(position),
Color::BLACK,
Some(index),
None,
)
}
fn horizontal_guide(position: f32, index: usize) -> Guide {
Guide::new(GuideKind::Horizontal(position), Color::BLACK, Some(index))
Guide::new(
GuideKind::Horizontal(position),
Color::BLACK,
Some(index),
None,
)
}
fn pool_with(guides: Vec<Guide>) -> GuidePool {

View File

@ -29,14 +29,25 @@ pub struct Guide {
pub color: Color,
/// Index of the guide in the guide list (clojure side)
pub index: usize,
/// When the guide belongs to a board, the `[start, end]` range (along the
/// guide's line direction) of that board. The guide is drawn solid only
/// within this range and trimmed outside it. `None` for free guides, which
/// span the whole viewport.
pub frame_range: Option<(f32, f32)>,
}
impl Guide {
pub fn new(kind: GuideKind, color: Color, index: Option<usize>) -> Self {
pub fn new(
kind: GuideKind,
color: Color,
index: Option<usize>,
frame_range: Option<(f32, f32)>,
) -> Self {
Self {
kind,
color,
index: index.unwrap_or_default(),
frame_range,
}
}

View File

@ -29,6 +29,9 @@ impl From<u32> for RawGuideKind {
///
/// The layout uses only 32-bit fields so it can be written from ClojureScript
/// straight into the `HEAPU32`/`HEAPF32` views without padding surprises.
///
/// `frame_start` / `frame_end` carry the board clip range (along the guide's
/// line direction). When the guide is not bound to a board they are `NaN`.
#[repr(C)]
#[repr(align(4))]
#[derive(Debug, Clone, PartialEq, Copy)]
@ -36,6 +39,8 @@ pub struct RawGuide {
kind: u32,
color: u32,
position: f32,
frame_start: f32,
frame_end: f32,
}
impl From<RawGuide> for Guide {
@ -44,7 +49,12 @@ impl From<RawGuide> for Guide {
RawGuideKind::Vertical => GuideKind::Vertical(value.position),
RawGuideKind::Horizontal => GuideKind::Horizontal(value.position),
};
Guide::new(kind, value.color.into(), None)
let frame_range = if value.frame_start.is_nan() || value.frame_end.is_nan() {
None
} else {
Some((value.frame_start, value.frame_end))
};
Guide::new(kind, value.color.into(), None, frame_range)
}
}