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