Pure scene-mutation and hit-test operations for the editor — the server-authoritative core the editor LiveComponent and its JS hook drive. Keeping them pure (no LiveView, no DOM) makes the editing model fully testable without a browser; the interactive layer is a thin shell over these.
All coordinates are canvas pixels (the same space scenes use). The drag gestures the browser reports are converted to canvas deltas by the hook, then applied here.
Hit-testing must run against the resolved layout (the scene after
OpenFresco.Layout.resolve/3) so it agrees with what was painted — anchored, placed, and auto-width elements paint somewhere their stored boxes aren't. The editor component does this; call sites doing their own hit-testing should too.
Summary
Functions
Move an element one step up in the paint order.
Move an element to the top of the z-order (rendered last / on top).
Apply a command, returning {new_scene, inverse_command} — applying the
inverse to new_scene restores the edit (undo). Group gestures use the
*_many commands so a multi-element drag is one undo step.
Remove an element by id.
Remove several elements by id.
The id of the top-most painted element containing {x, y} (canvas
px), or nil. Ties on z resolve exactly like the paint pass: later list
order paints later, i.e. on top.
All element ids whose (stored) boxes intersect the given rect — marquee select.
Insert an element at paint_index in the paint order (command/2's
delete-inverse). Z values are renumbered densely, like the stacking ops.
Translate an element's box by (dx, dy).
Translate several elements by the same (dx, dy) — one group move.
Move an element to position index in the paint order (0 = back-most;
anything ≥ the element count = front-most). All elements get dense z
values afterwards, so the order is deterministic.
Resize an element by dragging a corner handle (0 TL, 1 TR, 2 BR,
3 BL) by (dx, dy). Width/height are clamped to a small minimum and
normalized if a corner is dragged past its opposite.
Move an element one step down in the paint order.
Move an element to the bottom of the z-order (rendered first / behind).
Set an element's box directly (absolute canvas px), clamped/normalized.
Types
@type command() :: {:move, String.t(), number(), number()} | {:move_many, [String.t()], number(), number()} | {:resize, String.t(), 0..3, number(), number()} | {:set_box, String.t(), map()} | {:delete, String.t()} | {:delete_many, [String.t()]} | {:insert, OpenFresco.Scene.element(), non_neg_integer()} | {:insert_many, [{OpenFresco.Scene.element(), non_neg_integer()}]} | {:reorder, String.t(), integer()}
A command — the data form of an edit, applied via command/2, which
also returns the inverse command. Push inverses onto a stack and
hosts get undo (and redo, by inverting the inverse) for free.
Functions
@spec bring_forward(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()
Move an element one step up in the paint order.
@spec bring_to_front(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()
Move an element to the top of the z-order (rendered last / on top).
@spec command(OpenFresco.Scene.t(), command()) :: {OpenFresco.Scene.t(), command()}
Apply a command, returning {new_scene, inverse_command} — applying the
inverse to new_scene restores the edit (undo). Group gestures use the
*_many commands so a multi-element drag is one undo step.
{scene2, undo} = Ops.command(scene, {:move, "title", 10, 0})
{scene3, _redo} = Ops.command(scene2, undo) # back where it started
@spec delete(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()
Remove an element by id.
@spec delete_many(OpenFresco.Scene.t(), [String.t()]) :: OpenFresco.Scene.t()
Remove several elements by id.
@spec hit_test(OpenFresco.Scene.t(), number(), number()) :: String.t() | nil
The id of the top-most painted element containing {x, y} (canvas
px), or nil. Ties on z resolve exactly like the paint pass: later list
order paints later, i.e. on top.
@spec ids_in_rect(OpenFresco.Scene.t(), map()) :: [String.t()]
All element ids whose (stored) boxes intersect the given rect — marquee select.
@spec insert(OpenFresco.Scene.t(), OpenFresco.Scene.element(), non_neg_integer()) :: OpenFresco.Scene.t()
Insert an element at paint_index in the paint order (command/2's
delete-inverse). Z values are renumbered densely, like the stacking ops.
@spec move(OpenFresco.Scene.t(), String.t(), number(), number()) :: OpenFresco.Scene.t()
Translate an element's box by (dx, dy).
If the element carries a :place constraint, its numeric insets shift by
the same delta — otherwise the next layout pass would compute the placed
position from the old insets and snap the element straight back. An
:anchor's flow axis still wins over a drag (the anchor is the layout
authority there); hosts that want drag-to-detach should clear :anchor
themselves before applying the move.
@spec move_many(OpenFresco.Scene.t(), [String.t()], number(), number()) :: OpenFresco.Scene.t()
Translate several elements by the same (dx, dy) — one group move.
@spec reorder(OpenFresco.Scene.t(), String.t(), integer()) :: OpenFresco.Scene.t()
Move an element to position index in the paint order (0 = back-most;
anything ≥ the element count = front-most). All elements get dense z
values afterwards, so the order is deterministic.
@spec resize(OpenFresco.Scene.t(), String.t(), 0..3, number(), number()) :: OpenFresco.Scene.t()
Resize an element by dragging a corner handle (0 TL, 1 TR, 2 BR,
3 BL) by (dx, dy). Width/height are clamped to a small minimum and
normalized if a corner is dragged past its opposite.
@spec send_backward(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()
Move an element one step down in the paint order.
@spec send_to_back(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()
Move an element to the bottom of the z-order (rendered first / behind).
@spec set_box(OpenFresco.Scene.t(), String.t(), map()) :: OpenFresco.Scene.t()
Set an element's box directly (absolute canvas px), clamped/normalized.