moonlight-planar-1.1.0.0: test/curve/Moonlight/Planar/Curve/AuthoringSpec.hs
module Moonlight.Planar.Curve.AuthoringSpec (tests) where
import Data.Foldable (toList, traverse_)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Moonlight.Planar.Affine
( affine2, transformPoint, transformVector )
import Moonlight.Planar.Curve
( ClosedTrail, CurveStep, Located, OpenTrail, closedTrailSteps, curveStepEnd
, endJet, evaluateStep, joinContinuity, JoinContinuity (..), locatedValue
, location, openTrail, reverseClosedTrail, reverseLocatedTrail, startJet, trailDisplacement
, trailSteps, transformLocatedClosedTrail, transformLocatedTrail )
import Moonlight.Planar.Curve.Authoring
import Moonlight.Planar.Exact
( ExactPoint, ExactVector (..), exactHalf, exactPoint, exactPointCoordinates
, translateExactPoint, unitHalf, unitOne, unitZero )
import Support (assertEqual)
tests :: IO ()
tests = sequence_
[ testInterpolation
, testExplicitCorners
, testTensionAndReversal
, testAffineNaturality
, testProfiles
, testPolygonAndBow
, putStrLn "curve-authoring: ok"
]
landmarks :: NonEmpty ExactPoint
landmarks = exactPoint 0 0 :| [exactPoint 3 5, exactPoint 8 6, exactPoint 12 0]
testInterpolation :: IO ()
testInterpolation = do
let knots = Interpolating <$> landmarks
authored = cardinalOpen unitZero knots
closed = cardinalClosed unitZero knots
openSteps = toList (trailSteps (locatedValue authored))
cycleSteps = toList (closedTrailSteps (locatedValue closed))
assertEqual "open interpolation visits every landmark" (NonEmpty.toList landmarks) (anchors authored)
assertEqual "one span per consecutive open knot" 3 (length openSteps)
assertEqual "closed interpolation visits every landmark and closes"
(NonEmpty.toList landmarks <> [exactPoint 0 0]) (closedAnchors closed)
assertEqual "one span per cyclic knot" 4 (length cycleSteps)
assertEqual "regular interior C1 joins" [ParametricJoin, ParametricJoin]
(zipWith joinContinuity openSteps (drop 1 openSteps))
assertEqual "regular periodic C1 joins" (replicate 4 ParametricJoin)
(zipWith joinContinuity cycleSteps (drop 1 cycleSteps <> take 1 cycleSteps))
let single = cardinalOpen unitZero (Interpolating (exactPoint 7 9) :| [])
singleClosed = cardinalClosed unitZero (Interpolating (exactPoint 7 9) :| [])
assertEqual "singleton open retains location" [exactPoint 7 9] (anchors single)
assertEqual "singleton closed is constant" [exactPoint 7 9, exactPoint 7 9] (closedAnchors singleClosed)
assertEqual "repeated points require no divide or admission"
[exactPoint 2 3, exactPoint 2 3]
(anchors (cardinalOpen unitZero (Interpolating (exactPoint 2 3) :| [Interpolating (exactPoint 2 3)])))
testExplicitCorners :: IO ()
testExplicitCorners = do
let incoming = ExactVector 3 0
outgoing = ExactVector 0 6
knots = Interpolating (exactPoint 0 0) :|
[ExplicitJets (exactPoint 2 2) incoming outgoing, Interpolating (exactPoint 4 7)]
spans = toList (trailSteps (locatedValue (cardinalOpen unitOne knots)))
assertEqual "explicit corner survives maximum automatic tension" [CornerJoin]
(zipWith joinContinuity spans (drop 1 spans))
assertEqual "explicit incoming jet" [incoming] (endJet <$> take 1 spans)
assertEqual "explicit outgoing jet" [outgoing] (startJet <$> drop 1 spans)
let returning = cardinalClosed unitZero (ExplicitJets (exactPoint 0 0) incoming outgoing :| [])
assertEqual "single explicit closed knot retains returning cubic jets"
[(outgoing,incoming)]
((\step -> (startJet step,endJet step)) <$> toList (closedTrailSteps (locatedValue returning)))
testTensionAndReversal :: IO ()
testTensionAndReversal = do
let knots = Interpolating <$> landmarks
smooth = toList (trailSteps (locatedValue (cardinalOpen unitZero knots)))
tense = toList (trailSteps (locatedValue (cardinalOpen unitOne knots)))
half = toList (trailSteps (locatedValue (cardinalOpen unitHalf knots)))
assertEqual "maximum tension zeros automatic jets" (replicate 3 (zero,zero))
((\step -> (startJet step,endJet step)) <$> tense)
assertEqual "tension scales derived jets"
((\step -> (halfVector (startJet step),halfVector (endJet step))) <$> smooth)
((\step -> (startJet step,endJet step)) <$> half)
assertEqual "open interpolation commutes with reversal"
(reverseLocatedTrail (cardinalOpen unitHalf knots))
(cardinalOpen unitHalf (NonEmpty.reverse knots))
let initial :| remaining = knots
closed = cardinalClosed unitHalf knots
reversed = cardinalClosed unitHalf (initial :| reverse remaining)
assertEqual "closed reversal preserves authored first landmark" (location closed) (location reversed)
assertEqual "periodic interpolation commutes with anchored reversal"
(reverseClosedTrail (locatedValue closed)) (locatedValue reversed)
testAffineNaturality :: IO ()
testAffineNaturality = do
let transforms =
[ affine2 (ExactVector 2 1) (ExactVector (-3) 4) (ExactVector 7 8)
, affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 2 3)
, affine2 (ExactVector 1 2) (ExactVector 2 4) zero
]
knots = Interpolating (exactPoint 0 0) :|
[ExplicitJets (exactPoint 4 6) (ExactVector 3 2) (ExactVector (-2) 5), Interpolating (exactPoint 8 0)]
traverse_ (\transform -> do
let mapped = fmap (\knot -> case knot of
Interpolating point -> Interpolating (transformPoint transform point)
ExplicitJets point incoming outgoing -> ExplicitJets (transformPoint transform point)
(transformVector transform incoming) (transformVector transform outgoing)) knots
mappedStations = fmap (\(ProfileStation center width) ->
ProfileStation (transformPoint transform center) (transformVector transform width)) stations
assertEqual "cardinal open affine naturality"
(transformLocatedTrail transform (cardinalOpen unitHalf knots)) (cardinalOpen unitHalf mapped)
assertEqual "cardinal closed affine naturality"
(transformLocatedClosedTrail transform (cardinalClosed unitHalf knots)) (cardinalClosed unitHalf mapped)
assertEqual "profile affine naturality"
(transformLocatedClosedTrail transform (profileOutline unitHalf stations))
(profileOutline unitHalf mappedStations)) transforms
stations :: NonEmpty ProfileStation
stations = ProfileStation (exactPoint 0 0) zero :|
[ProfileStation (exactPoint 3 6) (ExactVector 4 1), ProfileStation (exactPoint 8 10) zero]
testProfiles :: IO ()
testProfiles = do
let (positive,negative) = profileRails unitZero stations
outline = profileOutline unitZero stations
centerKnots = fmap (\(ProfileStation center _) -> Interpolating center) stations
centerline = cardinalOpen unitZero centerKnots
samples = zip3 (locatedSteps positive) (locatedSteps negative) (locatedSteps centerline)
assertEqual "positive station interpolation"
[exactPoint 0 0, exactPoint 7 7, exactPoint 8 10] (anchors positive)
assertEqual "negative station interpolation"
[exactPoint 0 0, exactPoint (-1) 5, exactPoint 8 10] (anchors negative)
assertEqual "tapered root rails meet" (location positive) (location negative)
assertEqual "tapered tip rails meet" (endpoint positive) (endpoint negative)
assertEqual "profile outline derives closure" zero
(trailDisplacement (openTrail (closedTrailSteps (locatedValue outline))))
traverse_ (\(p,n,c) -> assertEqual "rail midpoint is interpolated centerline"
(twicePoint (sample c)) (sumPoints (sample p) (sample n))) samples
let reversedStations = NonEmpty.reverse stations
(reversePositive,reverseNegative) = profileRails unitZero reversedStations
assertEqual "positive rail reversal" (reverseLocatedTrail positive) reversePositive
assertEqual "negative rail reversal" (reverseLocatedTrail negative) reverseNegative
where
sample :: (ExactPoint,CurveStep) -> ExactPoint
sample (origin,step) = translateExactPoint origin (evaluateStep unitHalf step)
sumPoints :: ExactPoint -> ExactPoint -> ExactVector
sumPoints a b = let (ax,ay) = exactPointCoordinates a; (bx,by) = exactPointCoordinates b
in ExactVector (ax+bx) (ay+by)
twicePoint :: ExactPoint -> ExactVector
twicePoint point = sumPoints point point
testPolygonAndBow :: IO ()
testPolygonAndBow = do
let points = exactPoint 1 2 :| [exactPoint 5 2, exactPoint 5 7]
outline = polygonTrail points
positiveBow = bowedTrail exactHalf (exactPoint 1 2) (exactPoint 5 2)
negativeBow = bowedTrail (negate exactHalf) (exactPoint 1 2) (exactPoint 5 2)
assertEqual "polygon preserves input order and closes"
[exactPoint 1 2, exactPoint 5 2, exactPoint 5 7, exactPoint 1 2] (closedAnchors outline)
assertEqual "bow retains endpoints" [exactPoint 1 2,exactPoint 5 2] (anchors positiveBow)
assertEqual "positive bow exact midpoint sagitta" [exactPoint 3 4] (midpoints positiveBow)
assertEqual "negative bow exact midpoint sagitta" [exactPoint 3 0] (midpoints negativeBow)
assertEqual "bow reversal negates signed local bow" (reverseLocatedTrail positiveBow)
(bowedTrail (negate exactHalf) (exactPoint 5 2) (exactPoint 1 2))
assertEqual "zero chord remains constant" [exactPoint 2 3]
(midpoints (bowedTrail 100 (exactPoint 2 3) (exactPoint 2 3)))
anchors :: Located OpenTrail -> [ExactPoint]
anchors value = scanl (\point step -> translateExactPoint point (curveStepEnd step))
(location value) (toList (trailSteps (locatedValue value)))
closedAnchors :: Located ClosedTrail -> [ExactPoint]
closedAnchors value = scanl (\point step -> translateExactPoint point (curveStepEnd step))
(location value) (toList (closedTrailSteps (locatedValue value)))
locatedSteps :: Located OpenTrail -> [(ExactPoint,CurveStep)]
locatedSteps value = zip (anchors value) (toList (trailSteps (locatedValue value)))
endpoint :: Located OpenTrail -> ExactPoint
endpoint value = translateExactPoint (location value) (trailDisplacement (locatedValue value))
midpoints :: Located OpenTrail -> [ExactPoint]
midpoints = fmap (\(origin,step) -> translateExactPoint origin (evaluateStep unitHalf step)) . locatedSteps
halfVector :: ExactVector -> ExactVector
halfVector (ExactVector x y) = ExactVector (exactHalf*x) (exactHalf*y)
zero :: ExactVector
zero = ExactVector 0 0