# `OpenFresco.Editor.Ops`
[🔗](https://github.com/alexdont/open_fresco/blob/v0.2.0/lib/open_fresco/editor/ops.ex#L1)

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.

# `command`

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

# `bring_forward`

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

Move an element one step up in the paint order.

# `bring_to_front`

```elixir
@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`

```elixir
@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`

```elixir
@spec delete(OpenFresco.Scene.t(), String.t()) :: OpenFresco.Scene.t()
```

Remove an element by id.

# `delete_many`

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

Remove several elements by id.

# `hit_test`

```elixir
@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`

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

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

# `insert`

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

# `move`

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

# `move_many`

```elixir
@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`

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

# `resize`

```elixir
@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`

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

Move an element one step down in the paint order.

# `send_to_back`

```elixir
@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`

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

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
