Almost working polygon filling algorithm.
This commit is contained in:
parent
70c3b55483
commit
944e2f0e7f
5 changed files with 149 additions and 24 deletions
51
lib/enumerable/vivid/line.ex
Normal file
51
lib/enumerable/vivid/line.ex
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
@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
|
end
|
|
@ -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
|
||||||
|
|
4
test/enumerable/vivid/buffer_test.exs
Normal file
4
test/enumerable/vivid/buffer_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule Enumerable.Vivid.BufferTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Enumerable.Vivid.Buffer
|
||||||
|
end
|
4
test/enumerable/vivid/line_test.exs
Normal file
4
test/enumerable/vivid/line_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule Enumerable.Vivid.LineTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Enumerable.Vivid.Line
|
||||||
|
end
|
Loading…
Reference in a new issue