diff --git a/lib/enumerable/vivid/line.ex b/lib/enumerable/vivid/line.ex new file mode 100644 index 0000000..b1b057b --- /dev/null +++ b/lib/enumerable/vivid/line.ex @@ -0,0 +1,51 @@ +defimpl Enumerable, for: Vivid.Line do + alias Vivid.Line + + @moduledoc """ + Implements the Enumerable protocol for %Line{} + """ + + @doc """ + Returns the number of points on the line. + + ## Example + + iex> use Vivid + ...> Line.init(Point.init(1,1), Point.init(2,2)) + ...> |> Enum.count + 2 + """ + def count(%Line{}), do: {:ok, 2} + + @doc """ + Returns whether a point is one of this line's end points. + *note* not whether the point is *on* the line. + + ## Examples + + iex> use Vivid + ...> Line.init(Point.init(1,1), Point.init(2,2)) + ...> |> Enum.member?(Point.init(3,3)) + false + + iex> use Vivid + ...> Line.init(Point.init(1,1), Point.init(2,2)) + ...> |> Enum.member?(Point.init(2,2)) + true + """ + def member?(%Line{origin: p0}=_line, point) when p0 == point, do: {:ok, true} + def member?(%Line{termination: p0}=_line, point) when p0 == point, do: {:ok, true} + def member?(_line, _point), do: {:ok, false} + + @doc """ + Reduces the line's points into an accumulator + + ## Examples + + iex> use Vivid + ...> Line.init(Point.init(1,2), Point.init(2,4)) + ...> |> Enum.reduce(%{}, fn point, points -> Map.put(points, Point.x(point), Point.y(point)) end) + %{1 => 2, 2 => 4} + """ + def reduce(%Line{origin: p0, termination: p1}=_line, acc, fun), do: Enumerable.List.reduce([p0, p1], acc, fun) +end \ No newline at end of file diff --git a/lib/vivid/line.ex b/lib/vivid/line.ex index 80bceb8..23a3fb0 100644 --- a/lib/vivid/line.ex +++ b/lib/vivid/line.ex @@ -44,8 +44,10 @@ defmodule Vivid.Line do ## Example - iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(4,4)) |> Vivid.Line.termination - %Vivid.Point{x: 4, y: 4} + iex> use Vivid + ...> Line.init(Point.init(1,1), Point.init(4,4)) + ...> |> Line.termination + #Vivid.Point<{4, 4}> """ @spec termination(Line.t) :: Point.t def termination(%Line{termination: t}), do: t @@ -155,11 +157,7 @@ defmodule Vivid.Line do def x_intersect(%Line{termination: %Point{x: x0}=p}, x) when x0 == x, do: p def x_intersect(%Line{origin: %Point{x: x0, y: y0}, termination: %Point{x: x1, y: y1}}, x) when x0 < x and x < x1 do rx = (x - x0) / (x1 - x0) - y = if y1 > y0 do - rx * (y1 - y0) + y0 - else - rx * (y0 - y1) + y1 - end + y = rx * (y1 - y0) + y0 Point.init(x, y) end def x_intersect(_line, _x), do: nil @@ -183,12 +181,46 @@ defmodule Vivid.Line do def y_intersect(%Line{termination: %Point{y: y0}=p}, y) when y0 == y, do: p def y_intersect(%Line{origin: %Point{x: x0, y: y0}, termination: %Point{x: x1, y: y1}}, y) when y0 < y and y < y1 do ry = (y - y0) / (y1 - y0) - x = if x1 > x0 do - ry * (x1 - x0) + x0 - else - ry * (x0 - x1) + x1 - end + x = ry * (x1 - x0) + x0 Point.init(x, y) end def y_intersect(_line, _y), do: nil -end \ No newline at end of file + + @doc """ + Returns true if a line is horizontal. + + ## Example + + iex> use Vivid + ...> Line.init(Point.init(10,10), Point.init(20,10)) + ...> |> Line.horizontal? + true + + iex> use Vivid + ...> Line.init(Point.init(10,10), Point.init(20,11)) + ...> |> Line.horizontal? + false + """ + @spec horizontal?(Line.t) :: boolean + def horizontal?(%Line{origin: %Point{y: y0}, termination: %Point{y: y1}}) when y0 == y1, do: true + def horizontal?(_line), do: false + + @doc """ + Returns true if a line is vertical. + + ## Example + + iex> use Vivid + ...> Line.init(Point.init(10,10), Point.init(10,20)) + ...> |> Line.vertical? + true + + iex> use Vivid + ...> Line.init(Point.init(10,10), Point.init(11,20)) + ...> |> Line.vertical? + false + """ + @spec vertical?(Line.t) :: boolean + def vertical?(%Line{origin: %Point{x: x0}, termination: %Point{x: x1}}) when x0 == x1, do: true + def vertical?(_line), do: false +end diff --git a/lib/vivid/rasterize/polygon.ex b/lib/vivid/rasterize/polygon.ex index 452641b..fc02278 100644 --- a/lib/vivid/rasterize/polygon.ex +++ b/lib/vivid/rasterize/polygon.ex @@ -1,7 +1,12 @@ defimpl Vivid.Rasterize, for: Vivid.Polygon do - alias Vivid.{Polygon, Rasterize, Point} + alias Vivid.{Polygon, Rasterize, Point, Bounds, Line} require Integer + defmodule InvalidPolygonError do + @moduledoc false + defexception ~w(message)a + end + @moduledoc """ Rasterizes the Polygon into a sequence of points. """ @@ -24,24 +29,53 @@ defimpl Vivid.Rasterize, for: Vivid.Polygon do %Vivid.Point{x: 3, y: 3} ]) """ - def rasterize(%Polygon{fill: fill}=polygon, bounds) do + def rasterize(%Polygon{vertices: v}=_polygon, _bounds) when length(v) < 3 do + raise InvalidPolygonError, "Polygon does not contain enough edges." + end + + def rasterize(%Polygon{fill: false}=polygon, bounds) do lines = polygon |> Polygon.to_lines Enum.reduce(lines, MapSet.new, fn(line, acc) -> MapSet.union(acc, Rasterize.rasterize(line, bounds)) end) - |> fill(fill) end - def fill(points, false), do: points - def fill(points, true) do - points - |> Enum.sort_by(&Point.y(&1)) - |> Enum.chunk_by(&Point.y(&1)) - |> Enum.reduce(points, fn [p | _]=row, points -> - row = Enum.map(row, &Point.x(&1)) - reduce_x_fill(points, [], row, Point.y(p)) + def rasterize(%Polygon{fill: true}=polygon, bounds) do + range = polygon + |> Bounds.bounds + |> y_range + + lines = polygon + |> Polygon.to_lines + |> Enum.reject(&Line.horizontal?(&1)) + + points = Enum.reduce(range, MapSet.new, fn y, points -> + xs = lines + |> Stream.map(&Line.y_intersect(&1, y)) + |> Stream.reject(&is_nil(&1)) + |> Stream.map(&Point.x(&1)) + |> Stream.map(&round(&1)) + # |> Enum.dedup + |> Enum.sort + + MapSet.new + |> reduce_x_fill([], xs, y) + |> Stream.filter(&Bounds.contains?(bounds, &1)) + |> Enum.into(points) end) + + lines + |> Stream.flat_map(&Enum.to_list(&1)) + |> Stream.map(&Point.round(&1)) + |> Stream.filter(&Bounds.contains?(bounds, &1)) + |> Enum.reduce(points, fn point, points -> MapSet.put(points, point) end) + end + + defp y_range(bounds) do + y0 = bounds |> Bounds.min |> Point.y |> round + y1 = bounds |> Bounds.max |> Point.y |> round + if y1 > y0, do: y0..y1, else: y1..y0 end defp reduce_x_fill(points, _lhs, [], _y), do: points diff --git a/test/enumerable/vivid/buffer_test.exs b/test/enumerable/vivid/buffer_test.exs new file mode 100644 index 0000000..4ad158b --- /dev/null +++ b/test/enumerable/vivid/buffer_test.exs @@ -0,0 +1,4 @@ +defmodule Enumerable.Vivid.BufferTest do + use ExUnit.Case + doctest Enumerable.Vivid.Buffer +end \ No newline at end of file diff --git a/test/enumerable/vivid/line_test.exs b/test/enumerable/vivid/line_test.exs new file mode 100644 index 0000000..9143548 --- /dev/null +++ b/test/enumerable/vivid/line_test.exs @@ -0,0 +1,4 @@ +defmodule Enumerable.Vivid.LineTest do + use ExUnit.Case + doctest Enumerable.Vivid.Line +end \ No newline at end of file