packages feed

moonlight-planar-1.2.0.0: test/illustration/Moonlight/Planar/SpaceSwordSpec.hs

module Moonlight.Planar.SpaceSwordSpec (tests) where

import Control.Monad (unless)
import Data.Foldable (toList, traverse_)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Moonlight.Planar.Affine (affineColumns, affineIsoMap)
import Moonlight.Planar.Curve
  ( CurveStep, Located, OpenTrail, Path, Subpath (..), jetStep, locate, locatedValue
  , location, openTrail, path, splitStep, stepControlPoints, stepJetFirst, trailSteps
  , transformPath )
import Moonlight.Planar.Curve.Authoring (profileRails)
import Moonlight.Planar.Curve.Frame
  ( FrameSite (..), MeasuredFrame, measuredFrameIso, measuredFrameScaleSquared
  , measuredFrameSite, measuredFrameTangent )
import Moonlight.Planar.Curve.Measure
  ( ArcSample, MeasurePolicy, lengthBounds, lengthEnclosureLower, lengthEnclosureUpper
  , measureSubpath, sampleResidual, sampleSite, siteParameter, sitePoint, siteStepIndex )
import Moonlight.Planar.Exact
  ( ExactPoint, ExactRational, ExactVector (..), exactHalf, exactPoint, exactPointCoordinates
  , exactRational, unitHalf, unitIntervalValue, unitOne )
import Moonlight.Planar.Exhibit.SpaceSword
import Moonlight.Planar.Illustration (Picture, PictureAlgebra (..), foldPicture)

tests :: IO ()
tests = do
  let chargeOnly = defaultSpaceSwordControls { bladeCharge = unitOne }
      sweepOnly = defaultSpaceSwordControls { bladeSweep = 40 }
      blade = [BladeAura, BladeShell, BladeCore, BladeRunes, IonWake]
  baseline <- require (spaceSwordPicture defaultSpaceSwordControls)
  overcharged <- require (spaceSwordPicture overchargedSpaceSwordControls)
  charged <- require (spaceSwordPicture chargeOnly)
  swept <- require (spaceSwordPicture sweepOnly)
  traverse_ (checkPart baseline overcharged blade) [minBound .. maxBound]
  traverse_ (checkPart baseline charged [BladeAura, BladeCore, BladeRunes, IonWake]) [minBound .. maxBound]
  traverse_ (checkPart baseline swept blade) [minBound .. maxBound]
  check "semantic charge/sweep edit changes the sword" (baseline /= overcharged)
  base <- require (runeFrames defaultSpaceSwordControls)
  chargedRunes <- require (runeFrames chargeOnly)
  sweptRunes <- require (runeFrames sweepOnly)
  check "charge leaves the rune stations and frames fixed" (base == chargedRunes)
  check "sweep moves every rune frame"
    (and (NonEmpty.zipWith (\a b -> measuredFrameIso a /= measuredFrameIso b) base sweptRunes))
  traverse_ runeLaws [defaultSpaceSwordControls, overchargedSpaceSwordControls, chargeOnly, sweepOnly]
  traverse_ centerlineIsRailMidpoint [defaultSpaceSwordControls, overchargedSpaceSwordControls]
  putStrLn "space sword: complete typed composition, measured rune run and selective edits ok"

-- Station distance and frame scale are separate bounds. Each rune sample is
-- checked against an independent measurement of the centerline up to its
-- site; each frame against the jet of the centerline's own step there.
runeLaws :: SpaceSwordControls -> IO ()
runeLaws controls = do
  frames <- require (runeFrames controls)
  (measuring, _) <- require runeMeasurePolicy
  fractions <- traverse require [exactRational 1 5, exactRational 1 2, exactRational 4 5]
  let centerline = channelCenterline (unitIntervalValue (bladeCharge controls)) (bladeSweep controls)
  total <- lengthBounds <$> require (measureSubpath measuring (OpenSubpath centerline))
  samples <- traverse sampleOf (toList frames)
  check "three runes" (length samples == 3)
  check "runes follow the run's order along the centerline"
    (and (zipWith (<) (key <$> samples) (drop 1 (key <$> samples))))
  traverse_ (\(fraction, sample, frame) -> do
      let residual = sampleResidual sample
      check "station residual within the arc-length tolerance" (residual <= exactHalf ^ (3 :: Int))
      (lower, upper) <- prefixLength measuring centerline sample
      check "station distance agrees with an independent prefix measurement"
        (lower - fraction * lengthEnclosureUpper total <= residual
          && fraction * lengthEnclosureLower total - upper <= residual)
      tangent <- stepTangent centerline sample
      let (column, normal, offset) = affineColumns (affineIsoMap (measuredFrameIso frame))
          ExactVector cx cy = column
          (px, py) = exactPointCoordinates (sitePoint (sampleSite sample))
      check "frame tangent is the step's own jet" (measuredFrameTangent frame == tangent)
      check "frame tangent column points along the jet" (cross column tangent == 0 && dot column tangent > 0)
      check "frame columns are the tangent and its left normal" (normal == ExactVector (negate cy) cx)
      check "frame origin is the station" (offset == ExactVector px py)
      check "frame scale within its normalization bound"
        (dot column column == measuredFrameScaleSquared frame
          && measuredFrameScaleSquared frame <= 1
          && 1 - measuredFrameScaleSquared frame <= exactHalf ^ (20 :: Int)))
    (zip3 fractions samples (toList frames))
 where
  key :: ArcSample -> (Int, ExactRational)
  key sample = (siteStepIndex (sampleSite sample), unitIntervalValue (siteParameter (sampleSite sample)))

sampleOf :: MeasuredFrame -> IO ArcSample
sampleOf frame = case measuredFrameSite frame of
  SampledSite sample -> pure sample
  ExactSite _ -> fail "rune frame is not an arc-length station"

-- The centerline's steps before the sample, then its own step cut at the
-- sample's parameter, measured afresh with the same policy.
prefixLength :: MeasurePolicy -> Located OpenTrail -> ArcSample -> IO (ExactRational, ExactRational)
prefixLength measuring centerline sample = do
  step <- sampleCurveStep centerline sample
  let steps = trailSteps (locatedValue centerline)
      prefix = Seq.take (siteStepIndex (sampleSite sample)) steps Seq.|> fst (splitStep (siteParameter (sampleSite sample)) step)
  measured <- require (measureSubpath measuring (OpenSubpath (locate (location centerline) (openTrail prefix))))
  let bounds = lengthBounds measured
  pure (lengthEnclosureLower bounds, lengthEnclosureUpper bounds)

stepTangent :: Located OpenTrail -> ArcSample -> IO ExactVector
stepTangent centerline sample = do
  step <- sampleCurveStep centerline sample
  pure (stepJetFirst (jetStep (siteParameter (sampleSite sample)) step))

sampleCurveStep :: Located OpenTrail -> ArcSample -> IO CurveStep
sampleCurveStep centerline sample =
  maybe (fail "sample step outside the centerline") pure
    (Seq.lookup (siteStepIndex (sampleSite sample)) (trailSteps (locatedValue centerline)))

-- The derived centerline is the exact average of the channel's two rails at
-- the channel's tension, one half.
centerlineIsRailMidpoint :: SpaceSwordControls -> IO ()
centerlineIsRailMidpoint controls = do
  let charge = unitIntervalValue (bladeCharge controls)
      sweep = bladeSweep controls
      (positive, negative) = profileRails unitHalf (channelStations charge sweep)
      center = channelCenterline charge sweep
      controlsOf :: Located OpenTrail -> Seq [ExactVector]
      controlsOf = fmap (toList . stepControlPoints) . trailSteps . locatedValue
  check "centerline anchor is the rails' midpoint"
    (location center == midpointPoint (location positive) (location negative))
  check "centerline controls are the rails' midpoints"
    (Seq.length (controlsOf center) == Seq.length (controlsOf positive)
      && Seq.length (controlsOf center) == Seq.length (controlsOf negative)
      && controlsOf center == Seq.zipWith (zipWith midpointVector) (controlsOf positive) (controlsOf negative))

midpointPoint :: ExactPoint -> ExactPoint -> ExactPoint
midpointPoint a b =
  let (ax, ay) = exactPointCoordinates a
      (bx, by) = exactPointCoordinates b
   in exactPoint ((ax + bx) * exactHalf) ((ay + by) * exactHalf)

midpointVector :: ExactVector -> ExactVector -> ExactVector
midpointVector (ExactVector ax ay) (ExactVector bx by) =
  ExactVector ((ax + bx) * exactHalf) ((ay + by) * exactHalf)

cross :: ExactVector -> ExactVector -> ExactRational
cross (ExactVector a b) (ExactVector c d) = a * d - b * c

dot :: ExactVector -> ExactVector -> ExactRational
dot (ExactVector a b) (ExactVector c d) = a * c + b * d

checkPart
  :: Picture SpaceSwordPart
  -> Picture SpaceSwordPart
  -> [SpaceSwordPart]
  -> SpaceSwordPart
  -> IO ()
checkPart baseline edited affected part = do
  let original = observedPaths part baseline
      candidate = observedPaths part edited
  check ("nonempty space-sword part: " <> spaceSwordPartName part) (not (null original))
  check ("space-sword control dependency: " <> spaceSwordPartName part)
    ((original /= candidate) == (part `elem` affected))

-- A pure test projection of the existing Picture algebra. It preserves
-- placement and annotation ownership; it is not another geometry evaluator.
observedPaths :: SpaceSwordPart -> Picture SpaceSwordPart -> [Path]
observedPaths selected picture = foldPicture algebra picture Nothing
 where
  algebra :: PictureAlgebra SpaceSwordPart (Maybe SpaceSwordPart -> [Path])
  algebra = PictureAlgebra
    { paintSequence = \children inherited -> concatMap ($ inherited) (toList children)
    , paintFill = \_ _ contours inherited ->
        retain inherited (path (Seq.fromList (ClosedSubpath <$> NonEmpty.toList contours)))
    , paintStroke = \_ curve inherited -> retain inherited curve
    , paintClip = \_ _ child -> child
    , paintPlace = \frame child inherited -> transformPath frame <$> child inherited
    , paintOpacity = \_ child -> child
    , paintAnnotation = \part child _ -> child (Just part)
    }
  retain :: Maybe SpaceSwordPart -> Path -> [Path]
  retain inherited curve = if inherited == Just selected then [curve] else []

require :: Show e => Either e a -> IO a
require = either (fail . show) pure

check :: String -> Bool -> IO ()
check label condition = unless condition (fail label)