Bézier curves/Intersections: Difference between revisions

Content added Content deleted
(→‎{{header|Phix}}: A second Phix implementation)
Line 3,058: Line 3,058:
{-0.854984, 1.345017},
{-0.854984, 1.345017},
{ 0.881025, 1.118975}}
{ 0.881025, 1.118975}}
</pre>

===A Phix implementation of the "rectangle-pruned curve-flattening algorithm"===
{{trans|Pascal}}

This is the recursive version of the algorithm that appeared first (in non-recursive form) in the [[#Icon|Icon]] example.

<syntaxhighlight lang="phix">
-- -*- mode: indented-text; tab-width: 2; -*-

enum X, Y
enum C0, C1, C2
enum CURV, T0, T1, ENDPT0, ENDPT1

function normlength (atom a_x, a_y)
-- A "length" according to our chosen norm (which happens to be the
-- max norm). Here (a_x,a_y) is a vector used as measuring rod.
return max (abs (a_x), abs (a_y))
end function

function compare_normlengths (atom a_x, a_y, b_x, b_y)
-- Here is a general implementation for comparison of two
-- "normlength". For the euclidean norm, you might wish to skip
-- taking square roots.
atom len_a = normlength (a_x, a_y),
len_b = normlength (b_x, b_y),
cmpval = 0
if len_a < len_b then
cmpval = -1
elsif len_a > len_b then
cmpval = 1
end if
return cmpval
end function

function controls_to_spower (sequence controls)
-- Convert from control points (that is, Bernstein basis) to the
-- symmetric power basis.
sequence pt0 = controls[1],
pt1 = controls[2],
pt2 = controls[3]
return {{pt0[X], (2 * pt1[X]) - pt0[X] - pt2[X], pt2[X]},
{pt0[Y], (2 * pt1[Y]) - pt0[Y] - pt2[Y], pt2[Y]}}
end function

function spower_eval (sequence spow, atom t)
-- Evaluate an s-power spline at t.
return (spow[C0] + (spow[C1] * t)) * (1 - t) + (spow[C2] * t)
end function

function curve_eval (sequence curv, atom t)
-- Evaluate a curve at t.
return {spower_eval (curv[X], t),
spower_eval (curv[Y], t)}
end function

function spower_center_coef (sequence spow, atom t0, t1)
-- Return the center coefficient for the [t0,t1] portion of an
-- s-power spline. (The endpoint coefficients can be found with
-- spower_eval.) }
return spow[C1] * ((t1 - t0 - t0) * t1 + (t0 * t0))
end function

function spower_critical_pt (sequence spow)
-- Return t in (0,1) where p is at a critical point, else return -1.
atom crit_pt = -1
if spow[C1] != 0 then -- If c1 is zero, then the spline is linear.
if spow[C1] = spow[C2] then
crit_pt = 0.5 -- The spline is "pulse-like".
else
-- The critical point is the root of the derivative.
atom t = (0.5 * (spow[C2] + spow[C1] - spow[C0])) / spow[C1]
if (0 < t) and (t < 1) then
crit_pt = t
end if
end if
end if
return crit_pt
end function

function bisect_portion (sequence port)
-- Bisect a portion and pre-compute the new shared endpoint.
atom t_mid = 0.5 * (port[T0] + port[T1])
sequence pt_mid = curve_eval (port[CURV], t_mid)
return {{port[CURV], port[T0], t_mid, port[ENDPT0], pt_mid},
{port[CURV], t_mid, port[T1], pt_mid, port[ENDPT1]}}
end function

function rectangles_overlap (sequence a0, a1, b0, b1)
-- Do the rectangles with corners at (a0,a1) and (b0,b1) overlap at
-- all?
return ((min (a0[X], a1[X]) <= max (b0[X], b1[X]))
and (min (b0[X], b1[X]) <= max (a0[X], a1[X]))
and (min (a0[Y], a1[Y]) <= max (b0[Y], b1[Y]))
and (min (b0[Y], b1[Y]) <= max (a0[Y], a1[Y])))
end function

function segmentparameters (sequence a0, a1, b0, b1)
-- Return the respective [0,1] parameters of line segments (a0,a1)
-- and (b0,b1), for their intersection point. If there are not two
-- such parameters, return -1 for both values.
atom axdiff = a1[X] - a0[X],
aydiff = a1[Y] - a0[Y],
bxdiff = b1[X] - b0[X],
bydiff = b1[Y] - b0[Y],
denom = (axdiff * bydiff) - (aydiff * bxdiff),
anumer = ((bxdiff * a0[Y]) - (bydiff * a0[X])
+ (b0[X] * b1[Y]) - (b1[X] * b0[Y])),
ta = anumer / denom,
tb = -1
if (ta < 0.0) or (1.0 < ta) then
ta = -1
else
atom bnumer = -((axdiff * b0[Y]) - (aydiff * b0[X])
+ (a0[X] * a1[Y]) - (a1[X] * a0[Y]))
tb = bnumer / denom
if (tb < 0.0) or (1.0 < tb) then
ta = -1
tb = -1
end if
end if
return {ta, tb}
end function

function flat_enough (sequence port, atom tol)
-- Is a curve portion flat enough to be treated as a line segment
-- between its endpoints?

-- The degree-2 s-power polynomials are 1-t, t(1-t), t. We want to
-- remove the terms in t(1-t). The maximum of t(1-t) is 1/4, reached
-- at t=1/2. That accounts for the 1/4=0.25 in the following.

atom xcentercoef = spower_center_coef (port[CURV][X], port[T0],
port[T1]),
ycentercoef = spower_center_coef (port[CURV][Y], port[T0],
port[T1]),
xlen = port[ENDPT1][X] - port[ENDPT0][X],
ylen = port[ENDPT1][Y] - port[ENDPT0][Y]
return (compare_normlengths (0.25 * xcentercoef,
0.25 * ycentercoef,
tol * xlen, tol * ylen) <= 0)
end function

function find_portion_intersections (sequence pportion, qportion,
atom tol)
-- Find intersections between portions of two curves. Return pairs
-- of t-parameters. There may be duplicates and (due to truncation
-- error) near-intersections.
sequence intersections = {}
if rectangles_overlap (pportion[ENDPT0], pportion[ENDPT1],
qportion[ENDPT0], qportion[ENDPT1]) then
if flat_enough (pportion, tol) then
if flat_enough (qportion, tol) then
atom tp, tq
{tp, tq} = segmentparameters (pportion[ENDPT0],
pportion[ENDPT1],
qportion[ENDPT0],
qportion[ENDPT1])
if 0 <= tp then
tp = (1 - tp) * pportion[T0] + tp * pportion[T1]
tq = (1 - tq) * qportion[T0] + tq * qportion[T1]
intersections &= {{tp, tq}}
end if
else
sequence qport1, qport2
{qport1, qport2} = bisect_portion (qportion)
intersections &=
find_portion_intersections (pportion, qport1, tol)
intersections &=
find_portion_intersections (pportion, qport2, tol)
end if
else
if flat_enough (qportion, tol) then
sequence pport1, pport2
{pport1, pport2} = bisect_portion (pportion)
intersections &=
find_portion_intersections (pport1, qportion, tol)
intersections &=
find_portion_intersections (pport2, qportion, tol)
else
sequence pport1, pport2
sequence qport1, qport2
{pport1, pport2} = bisect_portion (pportion)
{qport1, qport2} = bisect_portion (qportion)
intersections &=
find_portion_intersections (pport1, qport1, tol)
intersections &=
find_portion_intersections (pport1, qport2, tol)
intersections &=
find_portion_intersections (pport2, qport1, tol)
intersections &=
find_portion_intersections (pport2, qport2, tol)
end if
end if
end if
return intersections
end function

function find_intersections (sequence p, q, atom tol)
-- Find intersections in [0,1]. Return pairs of t-parameters.
-- There may be duplicates and (due to truncation error)
-- near-intersections.

-- Break the curves at critical points, so one can assume the
-- portion between two endpoints is monotonic along both axes.

sequence tp = {0, 1}
atom tpx = spower_critical_pt (p[X]),
tpy = spower_critical_pt (p[Y])
if 0 < tpx then
tp &= {tpx}
end if
if 0 < tpy and tpy != tpx then
tp &= {tpy}
end if
tp = sort (tp)
sequence tq = {0, 1}

sequence pvals = {}
for t in tp do
pvals &= {curve_eval (p, t)}
end for

atom tqx = spower_critical_pt (q[X]),
tqy = spower_critical_pt (q[Y])
if 0 < tqx then
tq &= {tqx}
end if
if 0 < tqy and tqy != tqx then
tq &= {tqy}
end if
tq = sort (tq)

sequence qvals = {}
for t in tq do
qvals &= {curve_eval (q, t)}
end for

-- Find intersections in the cartesian product of the monotonic
-- portions of the two curves.
sequence intersections = {}
for i = 1 to length (tp) - 1 do
for j = 1 to length (tq) - 1 do
sequence pportion = {p, tp[i], tp[i + 1],
pvals[i], pvals[i + 1]},
qportion = {q, tq[j], tq[j + 1],
qvals[j], qvals[j + 1]}
intersections &=
find_portion_intersections (pportion, qportion, tol)
end for
end for

return intersections
end function

sequence pcontrols = {{-1, 0}, {0, 10}, {1, 0}},
qcontrols = {{2, 1}, {-8, 2}, {2, 3}},
p = controls_to_spower (pcontrols),
q = controls_to_spower (qcontrols)

atom flatness_tolerance = 0.0001

--
-- With this algorithm and its extension to higher degrees:
--
-- I suspect (albeit VERY, VERY, VERY weakly) merely removing exact
-- duplicates from the returned pairs of t-parameters will suffice to
-- eliminate repeated detections, because (aside from intersections
-- with multiplicities) these SHOULD occur only at endpoints, which
-- adjacent portions share, and where the t-parameters are exact zeros
-- and ones.
--
-- In any case, comparing t-parameters is certainly an alternative to
-- comparing point distances, especially if you want to let
-- intersections have multiplicity (as can easily happen with
-- cubics). Scheme's SRFI-1 has "delete-duplicates", which lets one
-- specify an equivalence predicate other than the default "equal?"--
-- the predicate can be defined as a closure to test closeness to
-- within some tolerance. That is how the code below SHOULD be
-- written.
--
-- But I do not know how to do the same thing so simply in Phix, and
-- thus will merely say "unique" here and let others update the code
-- if they wish. :)
--
sequence t_pairs =
unique (find_intersections (p, q, flatness_tolerance))

printf (1, "\n")
printf (1, " convex up ")
printf (1, " convex left\n")
for tt in t_pairs do
atom tp, tq
{tp, tq} = tt
sequence ppoint = curve_eval (p, tp),
qpoint = curve_eval (q, tq)
printf
(1, " %11.8f (%11.8f, %11.8f) %11.8f (%11.8f, %11.8f)\n",
{tp, ppoint[X], ppoint[Y], tq, qpoint[X], qpoint[Y]})
end for
printf (1, "\n")
</syntaxhighlight>

{{out}}
<pre>

convex up convex left
0.07250828 (-0.85498344, 1.34501661) 0.17250830 (-0.85498373, 1.34501660)
0.15948753 (-0.68102494, 2.68102517) 0.84051247 (-0.68102517, 2.68102494)
0.82749170 ( 0.65498340, 2.85498373) 0.92749172 ( 0.65498339, 2.85498344)
0.94051247 ( 0.88102493, 1.11897533) 0.05948753 ( 0.88102467, 1.11897507)

</pre>
</pre>