I implemented W3s recommended algorithm for converting SVG-path arcs from endpoint-arcs to center-arcs and back in Haskell.
type EndpointArc = ( Double, Double, Double, Double
, Bool, Bool, Double, Double, Double )
type CenterArc = ( Double, Double, Double, Double
, Double, Double, Double )
endpointToCenter :: EndpointArc -> CenterArc
centerToEndpoint :: CenterArc -> EndpointArc
See full implementation and test-code here.
But I can't get this property to pass:
import Test.QuickCheck
import Data.AEq ((~==))
instance Arbitrary EndpointArc where
arbitrary = do
((x1,y1),(x2,y2)) <- arbitrary `suchThat` (\(u,v) -> u /= v)
rx <- arbitrary `suchThat` (>0)
ry <- arbitrary `suchThat` (>0)
phi <- choose (0,2*pi)
(fA,fS) <- arbitrary
return $ correctRadiiSize (x1, y1, x2, y2, fA, fS, rx, ry, phi)
prop_conversionRetains :: EndpointArc -> Bool
prop_conversionRetains earc =
let result = centerToEndpoint (endpointToCenter earc)
in earc ~== result
Sometimes this is due to floating point errors (which seem to exceed ieee754) but sometimes there are NaNs in the result.
(NaN,NaN,NaN,NaN,False,False,1.0314334509082723,2.732814841776921,1.2776112657142984)
Which indicates there is no solution although I think I scale rx,ry as described in F.6.6.2 in W3's document.
import Numeric.Matrix
m :: [[Double]] -> Matrix Double
m = fromList
toTuple :: Matrix Double -> (Double, Double)
toTuple = (\[[x],[y]] -> (x,y)) . toList
primed :: Double -> Double -> Double -> Double -> Double
-> (Double, Double)
primed x1 y1 x2 y2 phi = toTuple $
m [[ cos phi, sin phi]
,[-sin phi, cos phi]
]
* m [[(x1 - x2)/2]
,[(y1 - y2)/2]
]
correctRadiiSize :: EndpointArc -> EndpointArc
correctRadiiSize (x1, y1, x2, y2, fA, fS, rx, ry, phi) =
let (x1',y1') = primed x1 y1 x2 y2 phi
lambda = (x1'^2/rx^2) + (y1'^2/ry^2)
(rx',ry') | lambda <= 1 = (rx, ry)
| otherwise = ((sqrt lambda) * rx, (sqrt lambda) * ry)
in (x1, y1, x2, y2, fA, fS, rx', ry', phi)
OK, I figured this out myself. The clue was of course in W3s document:
In the case that the radii are scaled up using equation (F.6.6.3), the radicand of (F.6.5.2) is zero and there is exactly one solution for the center of the ellipse.
F.6.5.2 in my code is
(cx',cy') = (sq * rx * y1' / ry, sq * (-ry) * x1' / rx)
where sq = negateIf (fA == fS) $ sqrt
$ ( rx^2 * ry^2 - rx^2 * y1'^2 - ry^2 * x1'^2 )
/ ( rx^2 * y1'^2 + ry^2 * x1'^2 )
The radicand that it is referring to is
( rx^2 * ry^2 - rx^2 * y1'^2 - ry^2 * x1'^2 )
/ ( rx^2 * y1'^2 + ry^2 * x1'^2 )
But of course, because we are working with floats it's not exactly zero but approximately and sometimes it might be something like -6.99496644301622e-17
which is negative! The square-root of a negative number is a complex number so the calculation returns NaN.
The trick really would be to propagate the fact that rx and ry have been resized to return zero and make sq
zero instead of going through the whole calculation unecessarily but the quick fix is just to take the absolute value of the radicand.
(cx',cy') = (sq * rx * y1' / ry, sq * (-ry) * x1' / rx)
where sq = negateIf (fA == fS) $ sqrt $ abs
$ ( rx^2 * ry^2 - rx^2 * y1'^2 - ry^2 * x1'^2 )
/ ( rx^2 * y1'^2 + ry^2 * x1'^2 )
After that there are some remaining floating point issues. Firstly the error exceeds what is allowed for by ieee754's ~==
operator so I made my own approxEq
approxEq (x1a, y1a, x2a, y2a, fAa, fSa, rxa, rya, phia) (x1b, y1b, x2b, y2b, fAb, fSb, rxb, ryb, phib) =
abs (x1a - x1b ) < 0.001
&& abs (y1a - y1b ) < 0.001
&& abs (x2a - x2b ) < 0.001
&& abs (y2a - y2b ) < 0.001
&& abs (y2a - y2b ) < 0.001
&& abs (rxa - rxb ) < 0.001
&& abs (rya - ryb ) < 0.001
&& abs (phia - phib) < 0.001
&& fAa == fAb
&& fSa == fSb
prop_conversionRetains :: EndpointArc -> Bool
prop_conversionRetains earc =
let result = centerToEndpoint (trace ("FIRST:" ++ show (endpointToCenter earc)) (endpointToCenter earc))
in earc `approxEq` trace ("SECOND:" ++ show result) result
Which starts bringing cases where fA is getting flipped. Spot the magic number:
FIRST:(-5.988957688551294,-39.5430169665332,64.95929681921707,29.661347617532357,5.939852349879405,-1.2436798376040206,3.141592653589793)
SECOND:(4.209851895761209,-73.01839718538467,-16.18776727286379,-6.067636747681732,False,True,64.95929681921707,29.661347617532357,5.939852349879405)
*** Failed! Falsifiable (after 20 tests):
(4.209851895761204,-73.01839718538467,-16.18776781572145,-6.0676366434916655,True,True,64.95929681921707,29.661347617532357,5.939852349879405)
You got it! fA = abs dtheta > pi
is in centerToEndpoint
so if it's therabouts then it can go either way.
So I took out the fA condition and increased the number of tests in quickcheck
approxEq (x1a, y1a, x2a, y2a, fAa, fSa, rxa, rya, phia) (x1b, y1b, x2b, y2b, fAb, fSb, rxb, ryb, phib) =
abs (x1a - x1b ) < 0.001
&& abs (y1a - y1b ) < 0.001
&& abs (x2a - x2b ) < 0.001
&& abs (y2a - y2b ) < 0.001
&& abs (y2a - y2b ) < 0.001
&& abs (rxa - rxb ) < 0.001
&& abs (rya - ryb ) < 0.001
&& abs (phia - phib) < 0.001
-- && fAa == fAb
&& fSa == fSb
main = quickCheckWith stdArgs {maxSuccess = 50000} prop_conversionRetains
Which shows that the threshold approxEq is still not lax enough.
approxEq (x1a, y1a, x2a, y2a, fAa, fSa, rxa, rya, phia) (x1b, y1b, x2b, y2b, fAb, fSb, rxb, ryb, phib) =
abs (x1a - x1b ) < 1
&& abs (y1a - y1b ) < 1
&& abs (x2a - x2b ) < 1
&& abs (y2a - y2b ) < 1
&& abs (y2a - y2b ) < 1
&& abs (rxa - rxb ) < 1
&& abs (rya - ryb ) < 1
&& abs (phia - phib) < 1
-- && fAa == fAb
&& fSa == fSb
Which I can finally get to pass reliably with a high number of tests. Well its all just to make some funny graphics anyway... I am sure it's accurate enough :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With