packages feed

moonlight-planar-1.1.0.0: src-dcel/Moonlight/Planar/Curve/Authoring.hs

-- | Exact interpolation and transverse profiles for semantic authoring.
-- These constructors immediately produce the canonical curve algebra; they
-- retain neither an evaluator nor a second spline representation. Uniform
-- knot spacing is a parameter convention, not arc length. Interpolation does
-- not guarantee simplicity, absence of overshoot, or preservation of topology.
module Moonlight.Planar.Curve.Authoring
  ( Knot (..)
  , cardinalOpen
  , cardinalClosed
  , ProfileStation (..)
  , profileRails
  , profileOutline
  , polygonTrail
  , bowedTrail
  ) where

import qualified Data.Foldable as Foldable
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Sequence as Seq
import Moonlight.Planar.Curve
  ( ClosedTrail, CurveStep, Located, OpenTrail, closeWith, curveStep
  , curveStepShape, hermiteStep, line, locate, locatedValue, location
  , openTrail, quadratic, reverseTrail, trailDisplacement )
import Moonlight.Planar.Exact
  ( ExactPoint, ExactRational, ExactVector (..), UnitInterval, addExactVectors
  , exactHalf, exactVectorFromPoints, translateExactPoint, unitIntervalValue )
import Moonlight.Planar.Internal.BoundaryCycle (consecutivePairs, cyclicTriples)

-- | An interpolated landmark or a landmark with independently prescribed
-- incoming and outgoing derivatives. Explicit derivatives ignore tension;
-- distinct directions make intentional corners without a global refit.
data Knot
  = Interpolating !ExactPoint
  | ExplicitJets !ExactPoint !ExactVector !ExactVector
  deriving stock (Eq, Show)

-- | Uniform cardinal interpolation. At tension zero, interior derivatives
-- are half the neighbor secant (Catmull-Rom). Endpoint derivatives use the
-- one-sided secant; tension one zeros only the automatically derived jets.
-- A singleton is a located empty trail; repeated points remain lawful input.
cardinalOpen :: UnitInterval -> NonEmpty Knot -> Located OpenTrail
cardinalOpen tension knots@(initial :| remaining) = assembleOpen (firstJet :| otherJets)
 where
  scale = 1 - unitIntervalValue tension
  firstJet = resolve (case remaining of
    [] -> zero
    next : _ -> scaleVector scale (between initial next)) initial
  otherJets = fmap derive
    (zip3 (NonEmpty.toList knots) remaining (fmap Just (drop 1 remaining) <> [Nothing]))
  derive :: (Knot, Knot, Maybe Knot) -> KnotJet
  derive (previous, current, next) = resolve automatic current
   where
    automatic = case next of
      Nothing -> scaleVector scale (between previous current)
      Just following -> scaleVector (scale * exactHalf) (between previous following)

-- | Periodic uniform cardinal interpolation, including the closing segment.
-- A singleton is a constant closed curve unless explicit nonzero jets specify
-- a returning cubic. No minimum polygon vertex count is invented here.
cardinalClosed :: UnitInterval -> NonEmpty Knot -> Located ClosedTrail
cardinalClosed tension knots@(initial :| _) =
  case fmap derive (cyclicTriples (NonEmpty.toList knots)) of
    [] -> assembleClosed (resolve zero initial :| [])
    firstJet : remaining -> assembleClosed (firstJet :| remaining)
 where
  scale = (1 - unitIntervalValue tension) * exactHalf
  derive :: (Knot, Knot, Knot) -> KnotJet
  derive (previous, current, next) = resolve (scaleVector scale (between previous next)) current

-- | A center landmark and a signed transverse half-span, not a Euclidean
-- unit normal. Width, taper, skew, and asymmetry remain rational and affine
-- equivariant. A zero half-span joins the two rails at that station.
data ProfileStation = ProfileStation !ExactPoint !ExactVector
  deriving stock (Eq, Show)

-- | Interpolate center + half-span and center - half-span with one tension.
-- The cardinal operator is affine-linear in the landmarks, so this is exactly
-- the interpolated centerline plus/minus the interpolated transverse profile.
profileRails
  :: UnitInterval -> NonEmpty ProfileStation -> (Located OpenTrail, Located OpenTrail)
profileRails tension stations =
  (cardinalOpen tension (fmap (Interpolating . positiveSide) stations),
   cardinalOpen tension (fmap (Interpolating . negativeSide) stations))

-- | Traverse the positive rail, connect the terminal stations, return along
-- the negative rail, and close at the root. End connectors are straight.
-- This is a transverse ribbon, not a constant-distance offset or a guarantee
-- that the outline is a simple region.
profileOutline :: UnitInterval -> NonEmpty ProfileStation -> Located ClosedTrail
profileOutline tension stations = locate (location positiveRail) (closeWith line prefix)
 where
  (positiveRail, negativeRail) = profileRails tension stations
  positiveTrail = locatedValue positiveRail
  negativeTrail = locatedValue negativeRail
  positiveEnd = translateExactPoint (location positiveRail) (trailDisplacement positiveTrail)
  negativeEnd = translateExactPoint (location negativeRail) (trailDisplacement negativeTrail)
  connector = openTrail (Seq.singleton (curveStep line (exactVectorFromPoints positiveEnd negativeEnd)))
  prefix = positiveTrail <> connector <> reverseTrail negativeTrail

-- | Straight-edged authored contour. Unlike Region.exactLoop, this preserves
-- source order and makes no simplicity or dimensionality claim.
polygonTrail :: NonEmpty ExactPoint -> Located ClosedTrail
polygonTrail points@(initial :| _) = locate initial (closeWith line (openTrail (Seq.fromList
  (fmap (\(a,b) -> curveStep line (exactVectorFromPoints a b))
    (consecutivePairs (NonEmpty.toList points))))))

-- | A quadratic bow whose midpoint displacement from the chord midpoint is
-- @bow * perpendicular(chord)@. Bow is dimensionless and signed. This is not
-- a circular arc and performs no irrational unit-normal normalization.
bowedTrail :: ExactRational -> ExactPoint -> ExactPoint -> Located OpenTrail
bowedTrail bow start end = locate start (openTrail (Seq.singleton
  (curveStep (quadratic control) chord)))
 where
  chord@(ExactVector x y) = exactVectorFromPoints start end
  control = addExactVectors (scaleVector exactHalf chord)
    (scaleVector (2 * bow) (ExactVector (negate y) x))

data KnotJet = KnotJet !ExactPoint !ExactVector !ExactVector

resolve :: ExactVector -> Knot -> KnotJet
resolve automatic knot = case knot of
  Interpolating point -> KnotJet point automatic automatic
  ExplicitJets point incoming outgoing -> KnotJet point incoming outgoing

knotPoint :: Knot -> ExactPoint
knotPoint (Interpolating point) = point
knotPoint (ExplicitJets point _ _) = point

between :: Knot -> Knot -> ExactVector
between a b = exactVectorFromPoints (knotPoint a) (knotPoint b)

jetSpan :: KnotJet -> KnotJet -> CurveStep
jetSpan (KnotJet a _ outgoing) (KnotJet b incoming _) =
  hermiteStep (exactVectorFromPoints a b) outgoing incoming

assembleOpen :: NonEmpty KnotJet -> Located OpenTrail
assembleOpen jets@(KnotJet anchor _ _ :| _) = locate anchor (openTrail (Seq.fromList
  (fmap (uncurry jetSpan) (consecutivePairs (NonEmpty.toList jets)))))

assembleClosed :: NonEmpty KnotJet -> Located ClosedTrail
assembleClosed jets@(initial :| remaining) =
  let authored = assembleOpen jets
      final = Foldable.foldl' (\_ current -> current) initial remaining
   in locate (location authored)
        (closeWith (curveStepShape (jetSpan final initial)) (locatedValue authored))

positiveSide :: ProfileStation -> ExactPoint
positiveSide (ProfileStation center halfSpan) = translateExactPoint center halfSpan

negativeSide :: ProfileStation -> ExactPoint
negativeSide (ProfileStation center halfSpan) = translateExactPoint center (scaleVector (-1) halfSpan)

scaleVector :: ExactRational -> ExactVector -> ExactVector
scaleVector factor (ExactVector x y) = ExactVector (factor*x) (factor*y)

zero :: ExactVector
zero = ExactVector 0 0