# `Curves.Drawing`
[🔗](https://github.com/greetingsfellowhumans/curves/blob/main/lib/curves/drawing.ex#L1)

A picture read from an SVG with `Curves.from_svg/2`: its shapes as curves, in the same groups and
order, with everything else kept as markup, so `Curves.Svg` can draw it again as it was.

```elixir
drawing = Curves.from_svg(File.read!("logo.svg"))

# Every shape, wherever it is in the picture
for shape <- Curves.Drawing.shapes(drawing), subpath <- shape.subpaths do
  Curves.solve!(subpath.curve, 0.5)
end

# Draw it again, as designed
Curves.Svg.save!(drawing, "logo_again.svg")
```

## Changing it

The `Curves.Transform` functions move, scale and rotate a whole drawing, or a group or shape in
it, just like a curve. To change one part, find it by its `id`: `update/3` changes it with a
function, `get/2` and `put/3` take it out and put it back, and `update_in/3` and `get_in/2` follow
a path of ids, one inside the next. `by_id/1` does the same for Elixir's own `Kernel.get_in/2`,
`Kernel.update_in/3` and `Kernel.pop_in/2`. `update_shapes/2` and `update_curves/2` change every
shape or every curve.

```elixir
drawing
|> Curves.Transform.inc_position({10, 0})
|> Curves.Drawing.update_in(["car", "front_wheel"], &Curves.Transform.inc_rotation(&1, 90, around: :center))
|> Curves.Drawing.update_shapes(&put_in(&1.attributes["stroke"], "red"))
```

| Field | What it holds |
| --- | --- |
| `:width`, `:height` | the `<svg>` element's `width` and `height`, as written (they may have units), or `nil` |
| `:view_box` | its `viewBox` as `{min_x, min_y, width, height}`, or `nil` |
| `:read_with_flip_y` | whether `Curves.from_svg/2` was given `flip_y: true`. `Curves.Svg` uses it to flip the view box of an SVG that has a size but no `viewBox`. It says how the SVG was read, so `Curves.Transform.flip_y/2` doesn't change it. |
| `:attributes` | all of the `<svg>` element's own attributes, as strings |
| `:children` | what is in it, in order: a `Curves.Drawing.Shape` for each shape, a `Curves.Drawing.Group` for each group, and a `Curves.Drawing.Raw` for everything else that is kept as markup, like text |
| `:skipped` | everything that couldn't be read as curves, and why, so nothing goes missing silently |
| `:origin`, `:scale`, `:rotation` | how `Curves.Transform` has moved, scaled and rotated the whole drawing. They start at `{0.0, 0.0}`, `1.0` and `0.0`. |

Each entry in `:skipped` is a map saying what the element was, why it wasn't read as curves
(`:reason`), and what was kept of it (`:kept`):

```elixir
%{element: "text",   id: "title", reason: :not_curves, kept: :as_markup}
%{element: "script", id: nil,     reason: :unsafe,     kept: :no}
```

| `:kept` | Meaning |
| --- | --- |
| `:as_markup` | kept as a `Curves.Drawing.Raw`, so it is drawn again just as it was, but has no curves |
| `:partly` | some of it is kept as curves (path data with an error: the subpaths before the error) |
| `:no` | nothing is kept: it won't be drawn again (though for `:empty` and `:invalid`, SVG doesn't draw it either) |

So everything that won't appear when the drawing is drawn again is
`Enum.filter(drawing.skipped, &(&1.kept == :no))`.

| `:reason` | Meaning | `:kept` |
| --- | --- | --- |
| `:not_curves` | an element that isn't made of curves (`<text>`, `<image>`, `<use>`), or a nested `<svg>`, which Curves can't follow | `:as_markup` |
| `:unsafe` | `<script>` or `<foreignObject>`, which are never kept, so a drawing drawn again can't run anything | `:no` |
| `{:unsupported_unit, value}` | a size in `%`, `em`, `mm` and so on, which needs a screen to measure; only plain numbers and `px` are read | `:as_markup` |
| `{:invalid_transform, message}` | a `transform` that couldn't be read; the element (for a group, everything in it) isn't read as curves | `:as_markup` (`:no` for the `<svg>` itself) |
| `{:invalid_path, message}` | path data with an error in it; like a browser, the shape still has the subpaths before the error | `:partly` |
| `{:invalid, message}` | a shape SVG treats as an error, like a negative radius, or (`:partly`) a polyline with an odd number of coordinates | `:no` or `:partly` |
| `:empty` | a shape SVG doesn't draw, like a circle with radius `0` | `:no` |

# `child`

```elixir
@type child() ::
  Curves.Drawing.Shape.t() | Curves.Drawing.Group.t() | Curves.Drawing.Raw.t()
```

Something inside a drawing or a group.

# `container`

```elixir
@type container() :: t() | Curves.Drawing.Group.t()
```

A drawing, or a group in one: something with `:children`.

# `matrix`

```elixir
@type matrix() :: {float(), float(), float(), float(), float(), float()}
```

A transform, `{a, b, c, d, e, f}`, like SVG's `matrix(a, b, c, d, e, f)`.

# `node_with_id`

```elixir
@type node_with_id() :: Curves.Drawing.Shape.t() | Curves.Drawing.Group.t()
```

Something that can be found by its `id`.

# `skipped`

```elixir
@type skipped() :: %{
  element: String.t(),
  id: String.t() | nil,
  reason: term(),
  kept: :as_markup | :partly | :no
}
```

Something that couldn't be read as curves. See the tables above.

# `t`

```elixir
@type t() :: %Curves.Drawing{
  attributes: %{required(String.t()) =&gt; String.t()},
  children: [child()],
  height: String.t() | nil,
  origin: Curves.Types.point_tuple(),
  read_with_flip_y: boolean(),
  rotation: float(),
  scale: number() | {number(), number()},
  skipped: [skipped()],
  view_box: {float(), float(), float(), float()} | nil,
  width: String.t() | nil
}
```

# `by_id`

```elixir
@spec by_id(String.t()) :: Access.access_fun(container(), child())
```

An `Access` function for the shape or group with this `id`, to use with Elixir's own
`Kernel.get_in/2`, `Kernel.update_in/3` and `Kernel.pop_in/2` on a drawing.

`get_in/2` gives `nil` if there is no such id, while `update_in/3` and `pop_in/2` raise an
`ArgumentError`, like `update/3` and `delete/2`.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><g id="car"><circle id="wheel" r="1"/></g></svg>))
    iex> wheel = Curves.Drawing.by_id("wheel")
    iex> get_in(drawing, [Curves.Drawing.by_id("car"), wheel]).element
    :circle
    iex> drawing = update_in(drawing, [wheel], &Curves.Transform.set_scale(&1, 2))
    iex> Curves.Drawing.get(drawing, "wheel").scale
    2
    iex> {_wheel, drawing} = pop_in(drawing, [wheel])
    iex> Curves.Drawing.get(drawing, "wheel")
    nil

# `curves`

```elixir
@spec curves(container()) :: [Curves.Spline.Curve.t() | Curves.Polyline.Curve.t()]
```

Every curve in the drawing (or group), shape by shape and subpath by subpath, in order, where it
is drawn (see `shapes/1`).

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><circle r="1"/><path d="M0 0 L1 1 M5 5 L6 6"/></svg>))
    iex> drawing |> Curves.Drawing.curves() |> length()
    3

# `delete`

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

Takes the shape or group with this `id` out of the drawing. Raises an `ArgumentError` if there
is none.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><circle id="sun" r="3"/><rect width="1" height="1"/></svg>))
    iex> drawing |> Curves.Drawing.delete("sun") |> Curves.Drawing.shapes() |> Enum.map(& &1.element)
    [:rect]

# `fetch`

```elixir
@spec fetch(container(), String.t()) :: {:ok, node_with_id()} | :error
```

The shape or group with this `id`, anywhere in the drawing (or group), as `{:ok, node}`, or
`:error` if there is none. The first one wins if several share an id.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><g id="house"><rect id="door" width="1" height="2"/></g></svg>))
    iex> {:ok, door} = Curves.Drawing.fetch(drawing, "door")
    iex> door.element
    :rect
    iex> Curves.Drawing.fetch(drawing, "garden")
    :error

# `get`

```elixir
@spec get(container(), String.t()) :: node_with_id() | nil
```

The shape or group with this `id`, or `nil`. See `fetch/2`.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><circle id="sun" r="3"/></svg>))
    iex> Curves.Drawing.get(drawing, "sun").element
    :circle
    iex> Curves.Drawing.get(drawing, "moon")
    nil

# `get_in`

```elixir
@spec get_in(container(), [String.t()]) :: child() | nil
```

Like `get/2`, following a path of ids: each id is looked for inside the one before it. Returns
`nil` if any of them isn't there.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><g id="car"><circle id="wheel" r="1"/></g><circle id="wheel" r="9"/></svg>))
    iex> Curves.Drawing.get_in(drawing, ["car", "wheel"]).attributes["r"]
    "1"

# `put`

```elixir
@spec put(container(), String.t(), child()) :: container()
```

Puts `node` (a shape, group or raw markup) in place of the shape or group with this `id`. Raises
an `ArgumentError` if there is none. Handy to take a part out, change it, and put it back.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><circle id="sun" r="3"/></svg>))
    iex> sun = Curves.Drawing.get(drawing, "sun")
    iex> drawing = Curves.Drawing.put(drawing, "sun", Curves.Transform.flip_x(sun))
    iex> Curves.Drawing.get(drawing, "sun").scale
    {-1.0, 1.0}

# `shapes`

```elixir
@spec shapes(container()) :: [Curves.Drawing.Shape.t()]
```

Every shape in the drawing (or group), in order, including those inside groups, where it is
drawn: any moves, scaling and rotation of the drawing, its groups and the shape itself (see
`Curves.Transform`) are applied to its curves, and included in its `:transform`.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><circle r="1"/><g><rect width="2" height="1"/></g></svg>))
    iex> drawing |> Curves.Drawing.shapes() |> Enum.map(& &1.element)
    [:circle, :rect]
    iex> moved = Curves.Transform.inc_x(drawing, 10)
    iex> [circle | _] = Curves.Drawing.shapes(moved)
    iex> Curves.solve!(hd(circle.subpaths).curve, 0.0)
    {11.0, 0.0}

# `update`

```elixir
@spec update(container(), String.t(), (node_with_id() -&gt; child())) :: container()
```

Changes the shape or group with this `id` (the first one, anywhere in the drawing) to what `fun`
returns for it. Raises an `ArgumentError` if there is none, or if `fun` doesn't return a shape,
group or raw markup.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><g id="wheel"><circle r="1"/></g></svg>))
    iex> drawing = Curves.Drawing.update(drawing, "wheel", &Curves.Transform.set_scale(&1, 3))
    iex> Curves.Drawing.get(drawing, "wheel").scale
    3

# `update_curves`

```elixir
@spec update_curves(
  container() | Curves.Drawing.Shape.t(),
  (Curves.Types.curve_struct() -&gt;
     Curves.Types.curve_struct())
) ::
  container() | Curves.Drawing.Shape.t()
```

Changes every curve in the drawing (or group, or shape) to what `fun` returns for it, e.g. to
apply a `Curves.Transform` to each curve on its own.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><path d="M0 0 L4 0"/></svg>))
    iex> drawing = Curves.Drawing.update_curves(drawing, &Curves.Transform.reverse/1)
    iex> drawing |> Curves.Drawing.curves() |> hd() |> Curves.solve!(0.0)
    {4.0, 0.0}

# `update_in`

```elixir
@spec update_in(container(), [String.t(), ...], (child() -&gt; child())) :: container()
```

Like `update/3`, following a path of ids: each id is looked for inside the one before it, and the
last one is changed by `fun`. Raises an `ArgumentError` if any of them isn't there, or if a shape
is asked for something inside it.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><g id="car"><g id="front"><circle id="wheel" r="1"/></g></g></svg>))
    iex> drawing = Curves.Drawing.update_in(drawing, ["car", "wheel"], &Curves.Transform.inc_rotation(&1, 90))
    iex> Curves.Drawing.get_in(drawing, ["car", "front", "wheel"]).rotation
    1.5707963267948966

# `update_shapes`

```elixir
@spec update_shapes(container(), (Curves.Drawing.Shape.t() -&gt;
                              Curves.Drawing.Shape.t())) :: container()
```

Changes every shape in the drawing (or group), wherever it is, to what `fun` returns for it.
The shapes are as stored: a group's transform isn't applied to them (see `shapes/1`). `fun` must
return a shape, group or raw markup; to take shapes out, use `delete/2`.

## Examples
    iex> drawing = Curves.from_svg(~s(<svg><circle r="1"/><g><rect width="2" height="1"/></g></svg>))
    iex> drawing = Curves.Drawing.update_shapes(drawing, &put_in(&1.attributes["fill"], "red"))
    iex> drawing |> Curves.Drawing.shapes() |> Enum.map(& &1.attributes["fill"])
    ["red", "red"]

---

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