lib/scenic/primitive/sector.ex

#
#  Created by Boyd Multerer on June 5, 2018.2017-10-29.
#  Copyright © 2017-2021 Kry10 Limited. All rights reserved.
#

defmodule Scenic.Primitive.Sector do
  @moduledoc """
  Draw an sector on the screen.

  An sector is a shape that looks like a piece of pie.

  ## Data

  `{radius, angle}`

  The data for an Sector is a tuple.
  * `radius` - the radius of the Sector
  * `angle` - the angle the Sector is swept through in radians

  ### Note

  The format for Sector has changed since v0.10. It used to be
  {radius, start_angle, end_angle}. You can achieve the same effect in the
  new, simpler format by using the same radius and the new angle is the
  difference between the old end_angle and start_angle. Then you can apply
  a rotation transform to get it in the right position.

  ## Styles

  This primitive recognizes the following styles
  * [`hidden`](Scenic.Primitive.Style.Hidden.html) - show or hide the primitive
  * [`scissor`](Scenic.Primitive.Style.Scissor.html) - "scissor rectangle" that drawing will be clipped to.
  * [`fill`](Scenic.Primitive.Style.Fill.html) - fill in the area of the primitive
  * [`stroke`](Scenic.Primitive.Style.Stroke.html) - stroke the outline of the primitive. In this case, only the curvy part.

  ## Usage

  You should add/modify primitives via the helper functions in
  [`Scenic.Primitives`](Scenic.Primitives.html#sector/3)

  ```elixir
  graph
    |> sector( {100, 1.5}, stroke: {1, :yellow} )
  ```
  """

  use Scenic.Primitive
  alias Scenic.Script
  alias Scenic.Primitive
  alias Scenic.Primitive.Style

  # import IEx

  @type t :: {radius :: number, angle :: number}
  @type styles_t :: [
          :hidden | :scissor | :fill | :stroke_width | :stroke_fill | :join | :miter_limit
        ]

  @styles [:hidden, :scissor, :fill, :stroke_width, :stroke_fill, :join, :miter_limit]

  @impl Primitive
  @spec validate(t()) :: {:ok, t()} | {:error, String.t()}
  def validate({radius, angle}) when is_number(radius) and is_number(angle) do
    {:ok, {radius, angle}}
  end

  def validate({r, a1, a2} = old) when is_number(r) and is_number(a1) and is_number(a2) do
    {
      :error,
      """
      #{IO.ANSI.red()}Invalid Sector specification
      Received: #{inspect(old)}
      #{IO.ANSI.yellow()}
      The data for an Sector has changed and is now {radius, angle}

      The old format went from a start angle to an end angle. You can achieve
      the same thing with just a single angle and a rotate transform.#{IO.ANSI.default_color()}
      """
    }
  end

  def validate(data) do
    {
      :error,
      """
      #{IO.ANSI.red()}Invalid Sector specification
      Received: #{inspect(data)}
      #{IO.ANSI.yellow()}
      The data for an Sector is {radius, angle}
      The radius must be >= 0#{IO.ANSI.default_color()}
      """
    }
  end

  # --------------------------------------------------------
  @doc """
  Returns a list of styles recognized by this primitive.
  """
  @impl Primitive
  @spec valid_styles() :: styles_t()
  def valid_styles(), do: @styles

  # --------------------------------------------------------
  @doc """
  Compile the data for this primitive into a mini script. This can be combined with others to
  generate a larger script and is called when a graph is compiled.
  """
  @spec compile(primitive :: Primitive.t(), styles :: Style.t()) :: Script.t()
  @impl Primitive
  def compile(%Primitive{module: __MODULE__, data: {radius, angle}}, styles) do
    Script.draw_sector([], radius, angle, Script.draw_flag(styles))
  end

  # --------------------------------------------------------
  def contains_point?({radius, angle}, {xp, yp}) do
    # using polar coordinates...
    point_angle = :math.atan2(yp, xp)
    point_radius_sqr = xp * xp + yp * yp

    # calculate the sector radius for that angle. Not just a simple
    # radius check as h and k get muliplied in to make it a sector
    # of an ellipse. Gotta check that too
    sx = radius * :math.cos(point_angle)
    sy = radius * :math.sin(point_angle)
    sector_radius_sqr = sx * sx + sy * sy

    if 0 <= angle do
      # clockwise winding
      point_angle >= 0 && point_angle <= angle && point_radius_sqr <= sector_radius_sqr
    else
      # counter-clockwise winding
      point_angle <= 0 && point_angle >= angle && point_radius_sqr <= sector_radius_sqr
    end
  end

  # --------------------------------------------------------
  # Math.matrix()
  @tau :math.pi() * 2
  def bounds(arc_data, mx \\ nil)

  def bounds(arc_data, nil) do
    bounds(arc_data, Scenic.Math.Matrix.identity())
  end

  def bounds({radius, angle}, <<_::binary-size(64)>> = mx) do
    n =
      cond do
        angle < @tau / 4 -> 4
        angle < @tau / 2 -> 8
        angle < @tau * 3 / 4 -> 12
        true -> 16
      end

    Enum.reduce(0..n, [{0, 0}], fn i, pts ->
      [{radius * :math.cos(angle * i / n), radius * :math.sin(angle * i / n)} | pts]
    end)
    |> Scenic.Math.Vector2.project(mx)
    |> Scenic.Math.Vector2.bounds()
  end
end