Skip to main content

src/svg_path@bezier.erl

-module(svg_path@bezier).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/svg_path/bezier.gleam").
-export([linear_bezier_data/2, quadratic_bezier_data/3, cubic_bezier_data/4, bezier_start/1, bezier_end/1, bezier_point/2, bezier_derivative/2, bezier_bounding_box/1, map_points/2, split_bezier/2, split_bezier_inside/2, split_bezier_many/2, split_bezier_inside_many/2, cubic_inflection_parameters/1]).
-export_type([point/0, bounding_box/0, bezier_data/0, error/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Lower-level helpers for Bezier curves.\n"
    "\n"
    " Most users should work with `svg_path.Line`, `svg_path.QuadraticBezier`,\n"
    " and `svg_path.CubicBezier` values through the root module. This module is\n"
    " the more technical layer for users who want the curve math behind line and\n"
    " Bezier segments.\n"
    "\n"
    " A line segment is a degree-1 Bezier curve, a quadratic Bezier has one\n"
    " control point, and a cubic Bezier has two control points. All evaluation\n"
    " and splitting helpers use the standard Bezier parameter `t`:\n"
    "\n"
    " - `bezier_point(curve, at: 0.0)` is the curve start point.\n"
    " - `bezier_point(curve, at: 1.0)` is the curve end point.\n"
    " - `bezier_derivative(curve, at: t)` is the derivative with respect to `t`.\n"
    " - `split_bezier(curve, at: t)` preserves the curve degree and divides it\n"
    "   with de Casteljau's algorithm.\n"
    " - `map_points(curve, with: f)` maps the curve's defining points.\n"
    "\n"
    " The `at` value is not clamped. Values outside `0.0..1.0` extrapolate along\n"
    " the same polynomial curve. `split_bezier` follows the same unclamped policy;\n"
    " use `split_bezier_inside` when outside values should return an error.\n"
    " `split_bezier_many` and `split_bezier_inside_many` sort their split points,\n"
    " remove exact duplicates, and trim boundary `0.0` or `1.0` split points that\n"
    " would only create zero-length boundary curves.\n"
    "\n"
    " `map_points` maps the control points that define the curve. For nonlinear\n"
    " functions, this is not the exact image of every point on the rendered curve;\n"
    " it is the Bezier curve obtained by applying the function to the defining\n"
    " points.\n"
).

-type point() :: {point, float(), float()}.

-type bounding_box() :: {bounding_box, point(), point()}.

-type bezier_data() :: {linear_bezier_data, point(), point()} |
    {quadratic_bezier_data, point(), point(), point()} |
    {cubic_bezier_data, point(), point(), point(), point()}.

-type error() :: split_outside_bezier.

-file("src/svg_path/bezier.gleam", 65).
?DOC(" Create linear Bezier data.\n").
-spec linear_bezier_data(point(), point()) -> bezier_data().
linear_bezier_data(Start, End) ->
    {linear_bezier_data, Start, End}.

-file("src/svg_path/bezier.gleam", 70).
?DOC(" Create quadratic Bezier data.\n").
-spec quadratic_bezier_data(point(), point(), point()) -> bezier_data().
quadratic_bezier_data(Start, Control, End) ->
    {quadratic_bezier_data, Start, Control, End}.

-file("src/svg_path/bezier.gleam", 79).
?DOC(" Create cubic Bezier data.\n").
-spec cubic_bezier_data(point(), point(), point(), point()) -> bezier_data().
cubic_bezier_data(Start, Control1, Control2, End) ->
    {cubic_bezier_data, Start, Control1, Control2, End}.

-file("src/svg_path/bezier.gleam", 89).
?DOC(" Return the curve's start point.\n").
-spec bezier_start(bezier_data()) -> point().
bezier_start(Curve) ->
    case Curve of
        {linear_bezier_data, Start, _} ->
            Start;

        {quadratic_bezier_data, Start, _, _} ->
            Start;

        {cubic_bezier_data, Start, _, _, _} ->
            Start
    end.

-file("src/svg_path/bezier.gleam", 98).
?DOC(" Return the curve's end point.\n").
-spec bezier_end(bezier_data()) -> point().
bezier_end(Curve) ->
    case Curve of
        {linear_bezier_data, _, End} ->
            End;

        {quadratic_bezier_data, _, _, End} ->
            End;

        {cubic_bezier_data, _, _, _, End} ->
            End
    end.

-file("src/svg_path/bezier.gleam", 589).
-spec interpolate(point(), point(), float()) -> point().
interpolate(Start, End, T) ->
    {point,
        erlang:element(2, Start) + ((erlang:element(2, End) - erlang:element(
            2,
            Start
        ))
        * T),
        erlang:element(3, Start) + ((erlang:element(3, End) - erlang:element(
            3,
            Start
        ))
        * T)}.

-file("src/svg_path/bezier.gleam", 111).
?DOC(
    " Evaluate a Bezier curve at parameter `t`.\n"
    "\n"
    " `t` is not clamped. `0.0` evaluates the start of the curve, `1.0` evaluates\n"
    " the end of the curve, and values outside that range extrapolate along the\n"
    " same polynomial curve.\n"
).
-spec bezier_point(bezier_data(), float()) -> point().
bezier_point(Curve, T) ->
    case Curve of
        {linear_bezier_data, Start, End} ->
            interpolate(Start, End, T);

        {quadratic_bezier_data, Start@1, Control, End@1} ->
            interpolate(
                interpolate(Start@1, Control, T),
                interpolate(Control, End@1, T),
                T
            );

        {cubic_bezier_data, Start@2, Control1, Control2, End@2} ->
            Left = interpolate(Start@2, Control1, T),
            Middle = interpolate(Control1, Control2, T),
            Right = interpolate(Control2, End@2, T),
            interpolate(
                interpolate(Left, Middle, T),
                interpolate(Middle, Right, T),
                T
            )
    end.

-file("src/svg_path/bezier.gleam", 604).
-spec scale(point(), float()) -> point().
scale(Point, Factor) ->
    {point,
        erlang:element(2, Point) * Factor,
        erlang:element(3, Point) * Factor}.

-file("src/svg_path/bezier.gleam", 596).
-spec difference(point(), point()) -> point().
difference(Left, Right) ->
    {point,
        erlang:element(2, Left) - erlang:element(2, Right),
        erlang:element(3, Left) - erlang:element(3, Right)}.

-file("src/svg_path/bezier.gleam", 136).
?DOC(" Return the derivative with respect to Bezier parameter `t`.\n").
-spec bezier_derivative(bezier_data(), float()) -> point().
bezier_derivative(Curve, T) ->
    case Curve of
        {linear_bezier_data, Start, End} ->
            difference(End, Start);

        {quadratic_bezier_data, Start@1, Control, End@1} ->
            scale(
                interpolate(
                    difference(Control, Start@1),
                    difference(End@1, Control),
                    T
                ),
                2.0
            );

        {cubic_bezier_data, Start@2, Control1, Control2, End@2} ->
            Left = difference(Control1, Start@2),
            Middle = difference(Control2, Control1),
            Right = difference(End@2, Control2),
            scale(
                interpolate(
                    interpolate(Left, Middle, T),
                    interpolate(Middle, Right, T),
                    T
                ),
                3.0
            )
    end.

-file("src/svg_path/bezier.gleam", 510).
-spec include_point(bounding_box(), point()) -> bounding_box().
include_point(Box, Point) ->
    {bounding_box,
        {point,
            gleam@float:min(
                erlang:element(2, erlang:element(2, Box)),
                erlang:element(2, Point)
            ),
            gleam@float:min(
                erlang:element(3, erlang:element(2, Box)),
                erlang:element(3, Point)
            )},
        {point,
            gleam@float:max(
                erlang:element(2, erlang:element(3, Box)),
                erlang:element(2, Point)
            ),
            gleam@float:max(
                erlang:element(3, erlang:element(3, Box)),
                erlang:element(3, Point)
            )}}.

-file("src/svg_path/bezier.gleam", 506).
-spec is_inside_unit_interval(float()) -> boolean().
is_inside_unit_interval(T) ->
    (T >= +0.0) andalso (T =< 1.0).

-file("src/svg_path/bezier.gleam", 499).
-spec linear_roots(float(), float()) -> list(float()).
linear_roots(A, B) ->
    case A =:= +0.0 of
        true ->
            [];

        false ->
            [case A of
                    +0.0 -> +0.0;
                    -0.0 -> -0.0;
                    Gleam@denominator -> (+0.0 - B) / Gleam@denominator
                end]
    end.

-file("src/svg_path/bezier.gleam", 447).
-spec quadratic_roots(float(), float(), float()) -> list(float()).
quadratic_roots(A, B, C) ->
    case A =:= +0.0 of
        true ->
            linear_roots(B, C);

        false ->
            Discriminant = (B * B) - ((4.0 * A) * C),
            case Discriminant < +0.0 of
                true ->
                    [];

                false ->
                    Root_discriminant@1 = case gleam@float:square_root(
                        Discriminant
                    ) of
                        {ok, Root_discriminant} -> Root_discriminant;
                        _assert_fail ->
                            erlang:error(#{gleam_error => let_assert,
                                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                        file => <<?FILEPATH/utf8>>,
                                        module => <<"svg_path/bezier"/utf8>>,
                                        function => <<"quadratic_roots"/utf8>>,
                                        line => 456,
                                        value => _assert_fail,
                                        start => 13837,
                                        'end' => 13903,
                                        pattern_start => 13848,
                                        pattern_end => 13869})
                    end,
                    Denominator = 2.0 * A,
                    case Root_discriminant@1 =:= +0.0 of
                        true ->
                            [case Denominator of
                                    +0.0 -> +0.0;
                                    -0.0 -> -0.0;
                                    Gleam@denominator -> (+0.0 - B) / Gleam@denominator
                                end];

                        false ->
                            [case Denominator of
                                    +0.0 -> +0.0;
                                    -0.0 -> -0.0;
                                    Gleam@denominator@1 -> ((+0.0 - B) - Root_discriminant@1)
                                    / Gleam@denominator@1
                                end, case Denominator of
                                    +0.0 -> +0.0;
                                    -0.0 -> -0.0;
                                    Gleam@denominator@2 -> ((+0.0 - B) + Root_discriminant@1)
                                    / Gleam@denominator@2
                                end]
                    end
            end
    end.

-file("src/svg_path/bezier.gleam", 409).
-spec cubic_extrema(float(), float(), float(), float()) -> list(float()).
cubic_extrema(Start, Control1, Control2, End) ->
    A = (((+0.0 - Start) + (3.0 * Control1)) - (3.0 * Control2)) + End,
    B = ((3.0 * Start) - (6.0 * Control1)) + (3.0 * Control2),
    C = (3.0 * Control1) - (3.0 * Start),
    quadratic_roots(3.0 * A, 2.0 * B, C).

-file("src/svg_path/bezier.gleam", 400).
-spec quadratic_extrema(float(), float(), float()) -> list(float()).
quadratic_extrema(Start, Control, End) ->
    Denominator = (Start - (2.0 * Control)) + End,
    case Denominator =:= +0.0 of
        true ->
            [];

        false ->
            [case Denominator of
                    +0.0 -> +0.0;
                    -0.0 -> -0.0;
                    Gleam@denominator -> (Start - Control) / Gleam@denominator
                end]
    end.

-file("src/svg_path/bezier.gleam", 383).
-spec bezier_extrema(bezier_data()) -> list(float()).
bezier_extrema(Curve) ->
    _pipe = case Curve of
        {linear_bezier_data, _, _} ->
            [];

        {quadratic_bezier_data, Start, Control, End} ->
            lists:append(
                quadratic_extrema(
                    erlang:element(2, Start),
                    erlang:element(2, Control),
                    erlang:element(2, End)
                ),
                quadratic_extrema(
                    erlang:element(3, Start),
                    erlang:element(3, Control),
                    erlang:element(3, End)
                )
            );

        {cubic_bezier_data, Start@1, Control1, Control2, End@1} ->
            lists:append(
                cubic_extrema(
                    erlang:element(2, Start@1),
                    erlang:element(2, Control1),
                    erlang:element(2, Control2),
                    erlang:element(2, End@1)
                ),
                cubic_extrema(
                    erlang:element(3, Start@1),
                    erlang:element(3, Control1),
                    erlang:element(3, Control2),
                    erlang:element(3, End@1)
                )
            )
    end,
    gleam@list:filter(_pipe, fun is_inside_unit_interval/1).

-file("src/svg_path/bezier.gleam", 163).
?DOC(" Return the curve's exact axis-aligned bounding box over `0.0..1.0`.\n").
-spec bezier_bounding_box(bezier_data()) -> bounding_box().
bezier_bounding_box(Curve) ->
    Points = begin
        _pipe = [+0.0, 1.0 | bezier_extrema(Curve)],
        gleam@list:map(_pipe, fun(T) -> bezier_point(Curve, T) end)
    end,
    {First@1, Rest@1} = case Points of
        [First | Rest] -> {First, Rest};
        _assert_fail ->
            erlang:error(#{gleam_error => let_assert,
                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                        file => <<?FILEPATH/utf8>>,
                        module => <<"svg_path/bezier"/utf8>>,
                        function => <<"bezier_bounding_box"/utf8>>,
                        line => 168,
                        value => _assert_fail,
                        start => 5378,
                        'end' => 5413,
                        pattern_start => 5389,
                        pattern_end => 5404})
    end,
    _pipe@1 = Rest@1,
    gleam@list:fold(
        _pipe@1,
        {bounding_box, First@1, First@1},
        fun include_point/2
    ).

-file("src/svg_path/bezier.gleam", 178).
?DOC(
    " Map a Bezier curve's defining points.\n"
    "\n"
    " For nonlinear functions, this is not the exact image of every point on the\n"
    " rendered curve. It maps the control polygon and preserves the curve degree.\n"
).
-spec map_points(bezier_data(), fun((point()) -> point())) -> bezier_data().
map_points(Curve, F) ->
    case Curve of
        {linear_bezier_data, Start, End} ->
            {linear_bezier_data, F(Start), F(End)};

        {quadratic_bezier_data, Start@1, Control, End@1} ->
            {quadratic_bezier_data, F(Start@1), F(Control), F(End@1)};

        {cubic_bezier_data, Start@2, Control1, Control2, End@2} ->
            {cubic_bezier_data, F(Start@2), F(Control1), F(Control2), F(End@2)}
    end.

-file("src/svg_path/bezier.gleam", 201).
?DOC(
    " Split a Bezier curve at parameter `t`.\n"
    "\n"
    " `t` is not clamped. Values outside `0.0..1.0` extrapolate along the same\n"
    " polynomial curve, matching `bezier_point`.\n"
).
-spec split_bezier(bezier_data(), float()) -> {bezier_data(), bezier_data()}.
split_bezier(Curve, T) ->
    case Curve of
        {linear_bezier_data, Start, End} ->
            Split = interpolate(Start, End, T),
            {{linear_bezier_data, Start, Split},
                {linear_bezier_data, Split, End}};

        {quadratic_bezier_data, Start@1, Control, End@1} ->
            Start_control = interpolate(Start@1, Control, T),
            Control_end = interpolate(Control, End@1, T),
            Split@1 = interpolate(Start_control, Control_end, T),
            {{quadratic_bezier_data, Start@1, Start_control, Split@1},
                {quadratic_bezier_data, Split@1, Control_end, End@1}};

        {cubic_bezier_data, Start@2, Control1, Control2, End@2} ->
            Start_control@1 = interpolate(Start@2, Control1, T),
            Controls = interpolate(Control1, Control2, T),
            Control_end@1 = interpolate(Control2, End@2, T),
            Left_control = interpolate(Start_control@1, Controls, T),
            Right_control = interpolate(Controls, Control_end@1, T),
            Split@2 = interpolate(Left_control, Right_control, T),
            {{cubic_bezier_data,
                    Start@2,
                    Start_control@1,
                    Left_control,
                    Split@2},
                {cubic_bezier_data,
                    Split@2,
                    Right_control,
                    Control_end@1,
                    End@2}}
    end.

-file("src/svg_path/bezier.gleam", 254).
?DOC(
    " Split a Bezier curve at parameter `t`, returning an error outside `0.0..1.0`.\n"
    "\n"
    " Values exactly at `0.0` or `1.0` are accepted and produce one zero-length\n"
    " curve.\n"
).
-spec split_bezier_inside(bezier_data(), float()) -> {ok,
        {bezier_data(), bezier_data()}} |
    {error, error()}.
split_bezier_inside(Curve, T) ->
    case (T < +0.0) orelse (T > 1.0) of
        true ->
            {error, split_outside_bezier};

        false ->
            {ok, split_bezier(Curve, T)}
    end.

-file("src/svg_path/bezier.gleam", 565).
-spec trim_reversed_end_progress(list(float())) -> list(float()).
trim_reversed_end_progress(Points) ->
    case Points of
        [1.0 | Rest] ->
            trim_reversed_end_progress(Rest);

        _ ->
            Points
    end.

-file("src/svg_path/bezier.gleam", 558).
-spec trim_end_progress(list(float())) -> list(float()).
trim_end_progress(Points) ->
    _pipe = Points,
    _pipe@1 = lists:reverse(_pipe),
    _pipe@2 = trim_reversed_end_progress(_pipe@1),
    lists:reverse(_pipe@2).

-file("src/svg_path/bezier.gleam", 551).
-spec trim_start_progress(list(float())) -> list(float()).
trim_start_progress(Points) ->
    case Points of
        [+0.0 | Rest] ->
            trim_start_progress(Rest);

        _ ->
            Points
    end.

-file("src/svg_path/bezier.gleam", 572).
-spec insert_unique_progress(list(float()), float()) -> list(float()).
insert_unique_progress(Sorted, Point) ->
    case Sorted of
        [] ->
            [Point];

        [First | Rest] ->
            case Point =:= First of
                true ->
                    Sorted;

                false ->
                    case Point =< First of
                        true ->
                            [Point | Sorted];

                        false ->
                            [First | insert_unique_progress(Rest, Point)]
                    end
            end
    end.

-file("src/svg_path/bezier.gleam", 517).
-spec sort_unique_progresses(list(float())) -> list(float()).
sort_unique_progresses(Points) ->
    case Points of
        [] ->
            [];

        [First | Rest] ->
            _pipe = sort_unique_progresses(Rest),
            insert_unique_progress(_pipe, First)
    end.

-file("src/svg_path/bezier.gleam", 376).
-spec normalized_progresses(list(float())) -> list(float()).
normalized_progresses(Points) ->
    _pipe = Points,
    _pipe@1 = sort_unique_progresses(_pipe),
    _pipe@2 = trim_start_progress(_pipe@1),
    trim_end_progress(_pipe@2).

-file("src/svg_path/bezier.gleam", 608).
-spec offset(point(), point(), float()) -> point().
offset(Point, Direction, Distance) ->
    {point,
        erlang:element(2, Point) + (erlang:element(2, Direction) * Distance),
        erlang:element(3, Point) + (erlang:element(3, Direction) * Distance)}.

-file("src/svg_path/bezier.gleam", 339).
-spec bezier_between(bezier_data(), float(), float()) -> bezier_data().
bezier_between(Curve, From, To) ->
    Start = bezier_point(Curve, From),
    End = bezier_point(Curve, To),
    Delta = To - From,
    case Curve of
        {linear_bezier_data, _, _} ->
            {linear_bezier_data, Start, End};

        {quadratic_bezier_data, _, _, _} ->
            {quadratic_bezier_data,
                Start,
                offset(Start, bezier_derivative(Curve, From), Delta / 2.0),
                End};

        {cubic_bezier_data, _, _, _, _} ->
            {cubic_bezier_data,
                Start,
                offset(Start, bezier_derivative(Curve, From), Delta / 3.0),
                offset(End, bezier_derivative(Curve, To), +0.0 - (Delta / 3.0)),
                End}
    end.

-file("src/svg_path/bezier.gleam", 319).
-spec split_bezier_between_progresses(
    bezier_data(),
    float(),
    list(float()),
    list(bezier_data())
) -> list(bezier_data()).
split_bezier_between_progresses(Curve, Previous, Points, Pieces) ->
    case Points of
        [] ->
            lists:reverse([bezier_between(Curve, Previous, 1.0) | Pieces]);

        [Next | Rest] ->
            split_bezier_between_progresses(
                Curve,
                Next,
                Rest,
                [bezier_between(Curve, Previous, Next) | Pieces]
            )
    end.

-file("src/svg_path/bezier.gleam", 312).
-spec split_bezier_at_progresses(bezier_data(), list(float())) -> list(bezier_data()).
split_bezier_at_progresses(Curve, Points) ->
    split_bezier_between_progresses(Curve, +0.0, Points, []).

-file("src/svg_path/bezier.gleam", 270).
?DOC(
    " Split a Bezier curve at multiple parameter values.\n"
    "\n"
    " Split points are sorted, exact duplicates are removed, and boundary `0.0`\n"
    " or `1.0` split points are trimmed when they would only create zero-length\n"
    " boundary curves. Values outside `0.0..1.0` are allowed and extrapolate along\n"
    " the same polynomial curve, matching `split_bezier`.\n"
).
-spec split_bezier_many(bezier_data(), list(float())) -> list(bezier_data()).
split_bezier_many(Curve, Points) ->
    split_bezier_at_progresses(Curve, normalized_progresses(Points)).

-file("src/svg_path/bezier.gleam", 282).
?DOC(
    " Split a Bezier curve at multiple parameter values, erroring outside `0.0..1.0`.\n"
    "\n"
    " Split points are sorted, exact duplicates are removed, and boundary `0.0`\n"
    " or `1.0` split points are trimmed when they would only create zero-length\n"
    " boundary curves. Values exactly at `0.0` or `1.0` are accepted.\n"
).
-spec split_bezier_inside_many(bezier_data(), list(float())) -> {ok,
        list(bezier_data())} |
    {error, error()}.
split_bezier_inside_many(Curve, Points) ->
    Points@1 = normalized_progresses(Points),
    case gleam@list:any(Points@1, fun(T) -> (T < +0.0) orelse (T > 1.0) end) of
        true ->
            {error, split_outside_bezier};

        false ->
            {ok, split_bezier_at_progresses(Curve, Points@1)}
    end.

-file("src/svg_path/bezier.gleam", 534).
-spec unique_close_progresses(list(float()), float(), list(float())) -> list(float()).
unique_close_progresses(Points, Previous, Kept) ->
    case Points of
        [] ->
            Kept;

        [Point | Rest] ->
            case gleam@float:absolute_value(Point - Previous) =< 0.000000001 of
                true ->
                    unique_close_progresses(Rest, Previous, Kept);

                false ->
                    unique_close_progresses(Rest, Point, [Point | Kept])
            end
    end.

-file("src/svg_path/bezier.gleam", 525).
-spec sort_unique_close_progresses(list(float())) -> list(float()).
sort_unique_close_progresses(Points) ->
    case gleam@list:sort(Points, fun gleam@float:compare/2) of
        [] ->
            [];

        [First | Rest] ->
            _pipe = unique_close_progresses(Rest, First, [First]),
            lists:reverse(_pipe)
    end.

-file("src/svg_path/bezier.gleam", 612).
-spec cross(point(), point()) -> float().
cross(Left, Right) ->
    (erlang:element(2, Left) * erlang:element(3, Right)) - (erlang:element(
        3,
        Left
    )
    * erlang:element(2, Right)).

-file("src/svg_path/bezier.gleam", 472).
-spec tolerant_quadratic_roots(float(), float(), float()) -> list(float()).
tolerant_quadratic_roots(A, B, C) ->
    case gleam@float:absolute_value(A) < 0.000000000001 of
        true ->
            case gleam@float:absolute_value(B) < 0.000000000001 of
                true ->
                    [];

                false ->
                    [case B of
                            +0.0 -> +0.0;
                            -0.0 -> -0.0;
                            Gleam@denominator -> (+0.0 - C) / Gleam@denominator
                        end]
            end;

        false ->
            Discriminant = (B * B) - ((4.0 * A) * C),
            case Discriminant < +0.0 of
                true ->
                    [];

                false ->
                    Root_discriminant@1 = case gleam@float:square_root(
                        Discriminant
                    ) of
                        {ok, Root_discriminant} -> Root_discriminant;
                        _assert_fail ->
                            erlang:error(#{gleam_error => let_assert,
                                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                        file => <<?FILEPATH/utf8>>,
                                        module => <<"svg_path/bezier"/utf8>>,
                                        function => <<"tolerant_quadratic_roots"/utf8>>,
                                        line => 486,
                                        value => _assert_fail,
                                        start => 14657,
                                        'end' => 14723,
                                        pattern_start => 14668,
                                        pattern_end => 14689})
                    end,
                    Denominator = 2.0 * A,
                    [case Denominator of
                            +0.0 -> +0.0;
                            -0.0 -> -0.0;
                            Gleam@denominator@1 -> ((+0.0 - B) - Root_discriminant@1)
                            / Gleam@denominator@1
                        end, case Denominator of
                            +0.0 -> +0.0;
                            -0.0 -> -0.0;
                            Gleam@denominator@2 -> ((+0.0 - B) + Root_discriminant@1)
                            / Gleam@denominator@2
                        end]
            end
    end.

-file("src/svg_path/bezier.gleam", 600).
-spec add(point(), point()) -> point().
add(Left, Right) ->
    {point,
        erlang:element(2, Left) + erlang:element(2, Right),
        erlang:element(3, Left) + erlang:element(3, Right)}.

-file("src/svg_path/bezier.gleam", 422).
-spec inflection_roots(point(), point(), point(), point()) -> list(float()).
inflection_roots(Start, Control1, Control2, End) ->
    A = add(
        difference(scale(Control1, 3.0), Start),
        difference(End, scale(Control2, 3.0))
    ),
    B = add(
        difference(scale(Start, 3.0), scale(Control1, 6.0)),
        scale(Control2, 3.0)
    ),
    C = difference(scale(Control1, 3.0), scale(Start, 3.0)),
    tolerant_quadratic_roots(
        -6.0 * cross(A, B),
        6.0 * cross(C, A),
        2.0 * cross(C, B)
    ).

-file("src/svg_path/bezier.gleam", 302).
?DOC(
    " Return the Bezier parameters of a cubic curve's inflection points.\n"
    "\n"
    " A cubic Bezier can have up to two inflection points. Values outside\n"
    " `0.0..1.0`, values too close to the endpoints, and numerically duplicate\n"
    " roots are not returned. Splitting at these parameters gives pieces with no\n"
    " interior inflection, which is often the useful first step before treating\n"
    " each piece as a convex curve plus its chord. Linear and quadratic curves\n"
    " return an empty list.\n"
).
-spec cubic_inflection_parameters(bezier_data()) -> list(float()).
cubic_inflection_parameters(Curve) ->
    case Curve of
        {cubic_bezier_data, Start, Control1, Control2, End} ->
            _pipe = inflection_roots(Start, Control1, Control2, End),
            _pipe@1 = gleam@list:filter(
                _pipe,
                fun(T) ->
                    (T > 0.000000001) andalso (T < (1.0 - 0.000000001))
                end
            ),
            sort_unique_close_progresses(_pipe@1);

        {linear_bezier_data, _, _} ->
            [];

        {quadratic_bezier_data, _, _, _} ->
            []
    end.