packages feed

tinytools-0.1.0.0: src/Potato/Flow/Controller/Manipulator/CartLine.hs

-- TODO DELETE ME
{-# OPTIONS_GHC -fno-warn-deprecations #-}
{-# LANGUAGE RecordWildCards #-}

module Potato.Flow.Controller.Manipulator.CartLine (
  CartLineHandler(..)
) where

import           Relude

import           Potato.Flow.Controller.Handler
import           Potato.Flow.Controller.Input
import           Potato.Flow.Controller.Manipulator.Common
import           Potato.Flow.Controller.Types
import           Potato.Flow.Math

import           Control.Exception
import           Data.Default
import qualified Text.Pretty.Simple as Pretty
import qualified Data.Text.Lazy as LT


{- examples of how CartLine works

1---2
    |
    3
drag 2 up (1 moves as well)
1---2
    |
    |
    3
drag 2 right (3 moves as well)
1------2
       |
       |
       3

1---x---2
drag x down (later anchor always moves)
1---*
    |
    *---2

examples:
1---3---2
drag 2 up
    3---2
        |
1-------*

-}

isCartesian :: XY -> XY -> Bool
isCartesian (V2 ax ay) (V2 bx by) = ax == bx || ay == by

-- | predicate holds if pt is between a and b
-- expects a b to be in the same cartesian plane
isBetween :: XY -> (XY, XY) -> Bool
isBetween (V2 px py) (a@(V2 ax ay), b@(V2 bx by)) = assert (isCartesian a b) $ if ax == bx && ax == px
  -- if in same vertical line
  then (py >= ay && py <= by) || (py <= ay && py >= by)
  else if ay == by && ay == py
    -- if in same horizontal line
    then (px >= ax && px <= bx) || (px <= ax && px >= bx)
    else False

splitFind :: (a -> Bool) -> [a] -> ([a],[a])
splitFind p l = r where
  splitFind' rprevs [] = (rprevs,[])
  splitFind' rprevs (x:xs) = if p x
    -- note we built up backwards but we reverse at the very end
    then (reverse rprevs, x:xs)
    else splitFind' (x:rprevs) xs
  r = splitFind' [] l

-- first elt of second list is currently selected anchor (no anchor selected if empty)
-- by assumption each anchor can only differ in one component from the previous one
-- anchors must not continue forward in same direction
-- not ok: 1----2----3
--     ok: 1-3--2
data AnchorZipper = AnchorZipper [XY] [XY] deriving (Show)
emptyAnchorZipper :: AnchorZipper
emptyAnchorZipper = AnchorZipper [] []

flattenAnchors :: AnchorZipper -> [XY]
flattenAnchors (AnchorZipper xs ys) = xs <> ys

-- | flatten AnchorZipper to a plain list
-- used only in creation step, where no anchor can be focused, asserts if this condition fails
flattenAnchorsInCreation :: AnchorZipper -> [XY]
flattenAnchorsInCreation az@(AnchorZipper xs ys) = assert (length ys == 0) $ flattenAnchors az


-- | adjacentPairs [1,2,3,4] `shouldBe` [(1,2),(2,3),(3,4)]
adjacentPairs :: [a] -> [(a,a)]
adjacentPairs [] = []
adjacentPairs (x:[]) = []
adjacentPairs (x:y:es) = (x,y) : adjacentPairs (y:es)


-- TODO TEST
-- | validate if AnchorZipper assumptions hold
validateAnchorZipper :: AnchorZipper -> Bool
validateAnchorZipper (AnchorZipper xs1 xs2) = r where

  check1 (V2 ex ey) (V2 l1x l1y) = if ex == l1x
    then ey /= l1y
    else ey == l1y
  check2 (V2 ex ey) (V2 l1x l1y) (V2 l2x l2y) = if l1x == l2x
    -- last one was vertical, expect horizontal or reversal
    then ey == l1y || l1x - l2x > ex - l2x
    -- last one was horizontal, expect vertical or reversal
    else ex == l1x || l1y - l2y > ey - l2y

  foldfn e (pass, mlast1, mlast2) = if not pass
    then (False, Nothing, Nothing)
    else case mlast1 of
      Just last1 -> case mlast2 of
        Just last2 -> (check2 e last1 last2 , Just e, Just last1)
        Nothing -> (check1 e last1, Just e, Just last1)
      Nothing -> (True, Just e, Nothing)

  (r, _, _) = foldr foldfn (True, Nothing, Nothing) (xs1<>xs2)

data CartLineHandler = CartLineHandler {
    _cartLineHandler_anchors      :: AnchorZipper
    , _cartLineHandler_undoFirst  :: Bool
    , _cartLineHandler_isCreation :: Bool
    , _cartLineHandler_active     :: Bool
  } deriving (Show)

instance Default CartLineHandler where
  def = CartLineHandler {
      _cartLineHandler_anchors = emptyAnchorZipper
      , _cartLineHandler_undoFirst = False
      , _cartLineHandler_isCreation = False
      , _cartLineHandler_active = False
    }


-- | get the last 2 elements of e1:e2:es
-- DELETE
last2 :: XY -> XY -> [XY] -> (XY, XY)
last2 e1 e2 es = r where
  l1 = last (e1:|e2:es)
  l2 = case (reverse es) of
    [] -> e1
    x:xs -> case xs of
      [] -> e2
      _ -> x
  r = (l1, l2)

-- helper method for creating new anchor at the end of a sequence of anchors (when creating new lines)
-- both input and output anchor list is REVERSED
elbowFromEnd :: XY -> [XY] -> [XY]
elbowFromEnd pos [] = [pos]
elbowFromEnd pos (e:[]) = r where
  V2 e1x e1y = e
  V2 dx dy = pos - e
  r = reverse $ if dx > dy
    then [e, V2 (e1x+dx) e1y] <> if dy == 0 then [] else [V2 (e1x+dx) (e1y + dy)]
    else [e, V2 e1x (e1y + dy)] <> if dx == 0 then [] else [V2 (e1x+dx) (e1y + dy)]
elbowFromEnd pos ls@(e1:(e2:es)) = r where
  V2 e1x e1y = e1
  V2 e2x e2y = e2
  V2 dx dy = pos - e1
  r = if dx == 0 && dy == 0
    then ls
    else if e1x == e2x
      -- if last was vertical
      then if dx == 0
        -- if there was no horizontal change, update the last point
        then pos:e2:es
        --last was vertical, go horizontal first
        else (if dy == 0 then [] else [V2 (e1x+dx) (e1y + dy)]) <> (V2 (e1x+dx) e1y : ls)
      -- last was horizontal
      else if dy == 0
        -- if there was no vertical change, update the last point
        then pos:e2:es
        --last was horizontal, go vertical first
        else (if dx == 0 then [] else [V2 (e1x+dx) (e1y + dy)]) <> (V2 e1x (e1y + dy) : ls)


smartAutoPathDown :: XY -> [XY] -> [XY]
smartAutoPathDown pos es = reverse $ elbowFromEnd pos (reverse es)




instance PotatoHandler CartLineHandler where
  pHandlerName _ = handlerName_cartesianLine
  pHandlerDebugShow clh = LT.toStrict $ Pretty.pShowNoColor clh
  pHandleMouse clh@CartLineHandler {..} PotatoHandlerInput {..} rmd@(RelMouseDrag MouseDrag {..}) = let

    -- restrict mouse
    dragDelta = _mouseDrag_to - _mouseDrag_from
    shiftClick = elem KeyModifier_Shift _mouseDrag_modifiers
    mousexy = _mouseDrag_from + if shiftClick
      then restrict4 dragDelta
      else dragDelta

    anchors = flattenAnchors _cartLineHandler_anchors

    in case _mouseDrag_state of
      -- if shift is held down, ignore inputs, this allows us to shift + click to deselect
      -- TODO consider moving this into GoatWidget since it's needed by many manipulators
      MouseDragState_Down | elem KeyModifier_Shift _mouseDrag_modifiers -> Nothing

      -- TODO creation should be a separate handler
      -- creation case
      MouseDragState_Down | _cartLineHandler_isCreation -> case _cartLineHandler_anchors of
        AnchorZipper _ (x:xs) -> error "this should never happen"
        AnchorZipper [] [] -> r where
          -- TODO track the fact we clicked, if we drag, pass on to SimpleLine? (but what happens if we drag back to start??)
          r = Just $ setHandlerOnly $ clh {
              _cartLineHandler_active = True
              , _cartLineHandler_anchors = AnchorZipper [mousexy] []
            }
        AnchorZipper (x:xs) [] -> if last (x :| xs) == mousexy
          -- if we click on the last dot, we're done, exit creation mode
          then Just $ setHandlerOnly $ clh {
              _cartLineHandler_isCreation = True
              , _cartLineHandler_active = False -- is it bad that we're still dragging but this is set to False?
            }
          -- otherwise, smartly path dot to destination (always make 90 degree bend from current if possible)
          else Just $ setHandlerOnly $ clh {
              _cartLineHandler_anchors = AnchorZipper
                (smartAutoPathDown mousexy (flattenAnchorsInCreation _cartLineHandler_anchors))
                []
            }
      -- TODO someday allow dragging dots on in creation case (to adjust position)
      MouseDragState_Dragging | _cartLineHandler_isCreation -> Just $ setHandlerOnly clh
      MouseDragState_Up | _cartLineHandler_isCreation ->  Just $ setHandlerOnly clh {
          -- disable creation mode on release (no reason besides it being convenient code wise)
          _cartLineHandler_isCreation = _cartLineHandler_active
        }

      -- modify existing line case
      MouseDragState_Down -> r where
        -- first go through and find dots we may have clicked on
        (dotfs,dotbs) = splitFind (== mousexy) anchors
        -- then go through and find any lines we may have clicked on
        (linefs, linebs) = splitFind (isBetween mousexy) (adjacentPairs anchors)

        r = if null dotbs
          -- we did not click on any dots
          then if null linebs
            -- we found nothing, input not captured
            then Nothing
            -- we clicked on a line
            else undefined -- TODO
          -- we clicked on a dot
          else undefined -- TODO

      -- TODO
      MouseDragState_Dragging -> r where
        r = undefined
      MouseDragState_Up -> r where
        -- on release cases, topology may change (some anchors removed), unclear how to map topology (probably need meta data to track)
        -- if release is on a dummy dot, (in between two other dots)
        -- TODO
        r = undefined

      MouseDragState_Cancelled -> Just def

  pHandleKeyboard clh PotatoHandlerInput {..} kbd = case kbd of
    -- TODO keyboard movement based on last selected manipulator I guess
    _                              -> Nothing

  pRenderHandler clh@CartLineHandler {..} PotatoHandlerInput {..} = r where
    toBoxHandle isactive xy = RenderHandle {
        _renderHandle_box = LBox xy 1
        , _renderHandle_char = if isactive then Just '+' else Just 'X'
        , _renderHandle_color = RHC_Default
      }
    AnchorZipper fronts' backs' = _cartLineHandler_anchors
    fronts = fmap (toBoxHandle False) fronts'
    backs = case backs' of
      [] -> []
      x:xs -> toBoxHandle True x : fmap (toBoxHandle False) fronts'
    r = HandlerRenderOutput (fronts <> backs)
  pIsHandlerActive = _cartLineHandler_active

  pHandlerTool CartLineHandler {..} = if _cartLineHandler_isCreation
    then Just Tool_CartLine
    else Nothing