Almost working polygon filling algorithm.

This commit is contained in:
James Harton 2017-01-13 15:53:48 +13:00
parent 70c3b55483
commit 944e2f0e7f
5 changed files with 149 additions and 24 deletions

View file

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

View file

@ -44,8 +44,10 @@ defmodule Vivid.Line do
## Example ## Example
iex> Vivid.Line.init(Vivid.Point.init(1,1), Vivid.Point.init(4,4)) |> Vivid.Line.termination iex> use Vivid
%Vivid.Point{x: 4, y: 4} ...> Line.init(Point.init(1,1), Point.init(4,4))
...> |> Line.termination
#Vivid.Point<{4, 4}>
""" """
@spec termination(Line.t) :: Point.t @spec termination(Line.t) :: Point.t
def termination(%Line{termination: t}), do: 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{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 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) rx = (x - x0) / (x1 - x0)
y = if y1 > y0 do y = rx * (y1 - y0) + y0
rx * (y1 - y0) + y0
else
rx * (y0 - y1) + y1
end
Point.init(x, y) Point.init(x, y)
end end
def x_intersect(_line, _x), do: nil 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{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 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) ry = (y - y0) / (y1 - y0)
x = if x1 > x0 do x = ry * (x1 - x0) + x0
ry * (x1 - x0) + x0
else
ry * (x0 - x1) + x1
end
Point.init(x, y) Point.init(x, y)
end end
def y_intersect(_line, _y), do: nil def y_intersect(_line, _y), do: nil
end
@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

View file

@ -1,7 +1,12 @@
defimpl Vivid.Rasterize, for: Vivid.Polygon do defimpl Vivid.Rasterize, for: Vivid.Polygon do
alias Vivid.{Polygon, Rasterize, Point} alias Vivid.{Polygon, Rasterize, Point, Bounds, Line}
require Integer require Integer
defmodule InvalidPolygonError do
@moduledoc false
defexception ~w(message)a
end
@moduledoc """ @moduledoc """
Rasterizes the Polygon into a sequence of points. 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} %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 lines = polygon |> Polygon.to_lines
Enum.reduce(lines, MapSet.new, fn(line, acc) -> Enum.reduce(lines, MapSet.new, fn(line, acc) ->
MapSet.union(acc, Rasterize.rasterize(line, bounds)) MapSet.union(acc, Rasterize.rasterize(line, bounds))
end) end)
|> fill(fill)
end end
def fill(points, false), do: points def rasterize(%Polygon{fill: true}=polygon, bounds) do
def fill(points, true) do range = polygon
points |> Bounds.bounds
|> Enum.sort_by(&Point.y(&1)) |> y_range
|> Enum.chunk_by(&Point.y(&1))
|> Enum.reduce(points, fn [p | _]=row, points -> lines = polygon
row = Enum.map(row, &Point.x(&1)) |> Polygon.to_lines
reduce_x_fill(points, [], row, Point.y(p)) |> 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) 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 end
defp reduce_x_fill(points, _lhs, [], _y), do: points defp reduce_x_fill(points, _lhs, [], _y), do: points

View file

@ -0,0 +1,4 @@
defmodule Enumerable.Vivid.BufferTest do
use ExUnit.Case
doctest Enumerable.Vivid.Buffer
end

View file

@ -0,0 +1,4 @@
defmodule Enumerable.Vivid.LineTest do
use ExUnit.Case
doctest Enumerable.Vivid.Line
end