OpenFresco.Editor.Ops (open_fresco v0.2.0)

Copy Markdown View Source

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

Types

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

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

command()

@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

bring_forward(scene, id)

@spec bring_forward(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()

Move an element one step up in the paint order.

bring_to_front(scene, id)

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

command(scene, arg)

@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

delete(scene, id)

Remove an element by id.

delete_many(scene, ids)

@spec delete_many(OpenFresco.Scene.t(), [String.t()]) :: OpenFresco.Scene.t()

Remove several elements by id.

hit_test(scene, x, y)

@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.

ids_in_rect(scene, map)

@spec ids_in_rect(OpenFresco.Scene.t(), map()) :: [String.t()]

All element ids whose (stored) boxes intersect the given rect — marquee select.

insert(scene, el, paint_index)

Insert an element at paint_index in the paint order (command/2's delete-inverse). Z values are renumbered densely, like the stacking ops.

move(scene, id, dx, dy)

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.

move_many(scene, ids, dx, dy)

@spec move_many(OpenFresco.Scene.t(), [String.t()], number(), number()) ::
  OpenFresco.Scene.t()

Translate several elements by the same (dx, dy) — one group move.

reorder(scene, id, index)

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(scene, id, corner, dx, dy)

@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.

send_backward(scene, id)

@spec send_backward(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()

Move an element one step down in the paint order.

send_to_back(scene, id)

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

set_box(scene, id, box)

@spec set_box(OpenFresco.Scene.t(), String.t(), map()) :: OpenFresco.Scene.t()

Set an element's box directly (absolute canvas px), clamped/normalized.