metabrush/src/metabrushes/MetaBrush/Action.hs

1057 lines
45 KiB
Haskell
Raw Normal View History

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module MetaBrush.Action
( -- * Subdividing a stroke
subdivide
-- * Selection
, selectAt, selectRectangle
-- * Drag
, dragMoveSelect, dragUpdate
-- * Translation
, translateSelection
-- * Deletion
, deleteSelected
-- * Brush widget actions
, BrushWidgetActionState(..)
, applyBrushWidgetAction
) where
-- base
import Control.Arrow
( second )
import Control.Monad
( guard, when )
2024-09-28 11:07:56 +00:00
import Control.Monad.ST
( RealWorld )
import Data.Foldable
( for_, traverse_ )
import Data.Functor
( (<&>) )
import qualified Data.List.NonEmpty as NE
import Data.Maybe
( fromMaybe, isNothing, listToMaybe )
import Data.Semigroup
( Arg(..), Min(..) )
import Data.Traversable
( for )
-- acts
import Data.Act
( Act(()), Torsor((-->)) )
-- containers
import qualified Data.Map.Strict as Map
import Data.Sequence
( Seq )
import qualified Data.Sequence as Seq
import Data.Set
( Set )
import qualified Data.Set as Set
-- generic-lens
import Data.Generics.Product.Fields
( field' )
-- groups
import Data.Group
( invert )
-- lens
import Control.Lens
( set, over )
-- transformers
import Control.Monad.Trans.Except
( Except )
import qualified Control.Monad.Trans.Except as Except
import Control.Monad.Trans.State.Strict
( State )
import qualified Control.Monad.Trans.State.Strict as State
import Control.Monad.Trans.Writer.CPS
( Writer )
import qualified Control.Monad.Trans.Writer.CPS as Writer
-- brush-strokes
import qualified Math.Bezier.Cubic as Cubic
import qualified Math.Bezier.Quadratic as Quadratic
import Math.Bezier.Spline
import Math.Bezier.Stroke
( invalidateCache )
import Math.Module
( lerp, quadrance, closestPointOnSegment
, squaredNorm
)
import Math.Linear
( Segment(..), (..), T(..) )
-- MetaBrush
import MetaBrush.Brush
import qualified MetaBrush.Brush.Widget as Brush
import MetaBrush.Document
import MetaBrush.Document.Diff
import MetaBrush.Hover
( inPointClickRange )
import MetaBrush.Records
import MetaBrush.Stroke
import MetaBrush.Unique
( Unique )
--------------------------------------------------------------------------------
-- Subdivision.
-- | Subdivide a path at the given center, provided a path indeed lies there.
subdivide :: 2 -> Document -> Maybe ( Document, Subdivision )
subdivide c doc@( Document { documentMetadata, documentContent }) =
let
updatedStrokes :: StrokeHierarchy
mbSubdivStroke :: Maybe Subdivision
( updatedStrokes, mbSubdivStroke )
= ( `State.runState` Nothing )
$ forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
subdivideStroke
in mbSubdivStroke <&> \ subdiv ->
( set ( field' @"documentContent" . field' @"strokeHierarchy" ) updatedStrokes
. set ( field' @"documentMetadata" . field' @"selectedPoints" ) mempty
$ doc
, subdiv )
where
Zoom { zoomFactor } = documentZoom documentMetadata
stripData :: Curve Open crvData ( PointData ptData ) -> Curve Open () ()
stripData = bimapCurve ( \ _ -> () ) ( \ _ _ -> () )
subdivideStroke :: Unique -> Stroke -> StrokeMetadata
-> State
( Maybe Subdivision )
UpdateStroke
subdivideStroke u stroke0@( Stroke { strokeSpline = strokeSpline :: StrokeSpline clo brushParams } ) ( StrokeMetadata { strokeVisible, strokeLocked } ) = do
mbPrevSubdivision <- State.get
let ( curves', subdivs ) =
Writer.runWriter $
bifoldSpline @Open
( subdivideCurve ( V2 0 0 ) )
( const $ pure Seq.Empty )
( adjustSplineType @Open strokeSpline )
if | strokeVisible
, not strokeLocked
, Nothing <- mbPrevSubdivision
, Just nonEmptySubdivs <- NE.nonEmpty subdivs
-> do State.put $ Just $ Subdivision u nonEmptySubdivs
let spline' = adjustSplineType @clo
$ strokeSpline { splineStart = splineStart strokeSpline
, splineCurves = OpenCurves curves' }
return $ UpdateStrokeTo stroke0 { strokeSpline = spline' }
| otherwise
-> return PreserveStroke
where
-- Note that if the user clicks on a point of self-intersection,
-- the code below will subdivide the curve on both branches.
-- This seems desirable.
subdivideCurve
:: T ( 2 )
-> PointData brushParams
2024-09-28 11:07:56 +00:00
-> Curve Open ( CurveData RealWorld ) ( PointData brushParams )
-> Writer [ ( Curve Open () (), Double ) ]
2024-09-28 11:07:56 +00:00
( Seq ( Curve Open ( CurveData RealWorld ) ( PointData brushParams ) ) )
subdivideCurve offset sp0 crv =
case crv of
LineTo ( NextPoint sp1 ) dat -> do
let
p0, p1, s :: 2
t :: Double
p0 = coords sp0
p1 = coords sp1
( t, s ) = closestPointOnSegment @( T ( 2 ) ) ( invert offset c ) ( Segment p0 p1 )
sqDist :: Double
sqDist = quadrance @( T ( 2 ) ) c ( offset s )
if t > 0 && t < 1 && sqDist * zoomFactor ^ ( 2 :: Int ) < 16
then do
Writer.tell [ ( stripData crv, t ) ]
let
subdiv :: PointData brushParams
subdiv = lerp @( DiffPointData ( T brushParams ) ) t sp0 sp1
return $
LineTo ( NextPoint subdiv ) ( invalidateCache dat )
Seq.:<| LineTo ( NextPoint sp1 ) ( invalidateCache dat )
Seq.:<| Seq.Empty
else return $ Seq.singleton crv
Bezier2To sp1 ( NextPoint sp2 ) dat -> do
let
p0, p1, p2 :: 2
p0 = coords sp0
p1 = coords sp1
p2 = coords sp2
sqDist :: Double
Min ( Arg sqDist ( t, _ ) )
= Quadratic.closestPoint @( T ( 2 ) ) ( Quadratic.Bezier {..} ) ( invert offset c )
if t > 0 && t < 1 && sqDist * zoomFactor ^ ( 2 :: Int ) < 16
then
case Quadratic.subdivide @( DiffPointData ( T brushParams ) ) ( Quadratic.Bezier sp0 sp1 sp2 ) t of
( Quadratic.Bezier _ q1 subdiv, Quadratic.Bezier _ r1 _ ) -> do
Writer.tell [ ( stripData crv, t ) ]
let
2024-09-28 11:07:56 +00:00
bez_start, bez_end :: Curve Open ( CurveData RealWorld ) ( PointData brushParams )
bez_start = Bezier2To q1 ( NextPoint subdiv ) ( invalidateCache dat )
bez_end = Bezier2To r1 ( NextPoint sp2 ) ( invalidateCache dat )
return ( bez_start Seq.:<| bez_end Seq.:<| Seq.Empty )
else return $ Seq.singleton crv
Bezier3To sp1 sp2 ( NextPoint sp3 ) dat -> do
let
p0, p1, p2, p3 :: 2
p0 = coords sp0
p1 = coords sp1
p2 = coords sp2
p3 = coords sp3
Min ( Arg sqDist ( t, _ ) )
= Cubic.closestPoint @( T ( 2 ) ) ( Cubic.Bezier {..} ) ( invert offset c )
if t > 0 && t < 1 && sqDist * zoomFactor ^ ( 2 :: Int ) < 16
then do
case Cubic.subdivide @( DiffPointData ( T brushParams ) ) ( Cubic.Bezier sp0 sp1 sp2 sp3 ) t of
( Cubic.Bezier _ q1 q2 subdiv, Cubic.Bezier _ r1 r2 _ ) -> do
Writer.tell [ ( stripData crv, t ) ]
let
2024-09-28 11:07:56 +00:00
bez_start, bez_end :: Curve Open ( CurveData RealWorld ) ( PointData brushParams )
bez_start = Bezier3To q1 q2 ( NextPoint subdiv ) ( invalidateCache dat )
bez_end = Bezier3To r1 r2 ( NextPoint sp3 ) ( invalidateCache dat )
return ( bez_start Seq.:<| bez_end Seq.:<| Seq.Empty )
else
return $ Seq.singleton crv
--------------------------------------------------------------------------------
-- Selection.
-- | Updates the selection on a single-click selection or deselection event.
selectAt :: SelectionMode -> 2 -> Document -> Maybe ( Document, StrokePoints )
selectAt selMode c doc@( Document { documentContent, documentMetadata } ) =
let mbSel =
Except.runExcept $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
computeSelected
selPts = case mbSel of
Left ( u, i ) -> StrokePoints $ Map.singleton u ( Set.singleton i )
Right {} -> mempty
in if noStrokePoints selPts && selMode /= New
then Nothing
else
let doc' =
over ( field' @"documentMetadata" . field' @"selectedPoints" )
( addOrRemove selMode selPts )
doc
in Just ( doc', selPts )
where
Zoom { zoomFactor } = documentZoom documentMetadata
computeSelected :: Unique -> Stroke -> StrokeMetadata -> Except ( Unique, PointIndex ) UpdateStroke
computeSelected strokeUnique ( Stroke { strokeSpline } ) ( StrokeMetadata { strokeVisible } ) = do
when ( strokeVisible ) $
Except.withExcept ( strokeUnique , ) $
bifoldSpline
( \ _ -> selectSplineCurve )
( selectSplinePoint FirstPoint )
strokeSpline
return PreserveStroke
selectSplineCurve :: SplineTypeI clo
2024-09-28 11:07:56 +00:00
=> Curve clo ( CurveData RealWorld ) ( PointData ptData )
-> Except PointIndex ()
selectSplineCurve = \case
LineTo p1 ( CurveData { curveIndex } ) ->
traverse_ ( selectSplinePoint ( PointIndex curveIndex PathPoint ) ) p1
Bezier2To cp1 p2 ( CurveData { curveIndex } ) -> do
selectSplinePoint ( PointIndex curveIndex ( ControlPoint Bez2Cp ) ) cp1
traverse_ ( selectSplinePoint ( PointIndex curveIndex PathPoint ) ) p2
Bezier3To cp1 cp2 p3 ( CurveData { curveIndex } ) -> do
selectSplinePoint ( PointIndex curveIndex ( ControlPoint Bez3Cp1 ) ) cp1
selectSplinePoint ( PointIndex curveIndex ( ControlPoint Bez3Cp2 ) ) cp2
traverse_ ( selectSplinePoint ( PointIndex curveIndex PathPoint ) ) p3
selectSplinePoint :: PointIndex -> PointData brushParams -> Except PointIndex ()
selectSplinePoint ptIx pt = do
let
selected :: Bool
selected =
squaredNorm ( c --> coords pt :: T ( 2 ) ) * zoomFactor ^ ( 2 :: Int ) < 16
when selected $
Except.throwE ptIx
-- | Checks whether a mouse click can initiate a drag move event.
dragMoveSelect :: 2 -> Document -> Maybe DragMoveSelect
dragMoveSelect c ( Document { documentContent, documentMetadata } ) =
( \case { Left drag -> Just drag; Right {} -> Nothing } ) $
Except.runExcept $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
dragSelect
where
Zoom { zoomFactor } = documentZoom documentMetadata
inSelectionRange :: 2 -> Bool
inSelectionRange p =
squaredNorm ( c --> p :: T ( 2 ) ) * zoomFactor ^ ( 2 :: Int ) < 16
dragSelect :: Unique -> Stroke -> StrokeMetadata -> Except DragMoveSelect UpdateStroke
dragSelect strokeUnique ( Stroke { strokeSpline } ) ( StrokeMetadata { strokeVisible, strokeLocked } ) = do
when ( strokeVisible && not strokeLocked ) $
ibifoldSpline
( dragSelectSplineCurve strokeUnique ( splineStart strokeSpline ) )
( dragSelectSplinePoint strokeUnique FirstPoint )
strokeSpline
return PreserveStroke
dragSelectSplineCurve
:: forall clo' brushParams
. ( SplineTypeI clo', Traversable ( NextPoint clo' ) )
=> Unique
-> PointData brushParams
-> Int
2024-09-28 11:07:56 +00:00
-> PointData brushParams -> Curve clo' ( CurveData RealWorld ) ( PointData brushParams )
-> Except DragMoveSelect ()
dragSelectSplineCurve uniq start i sp0 = \case
LineTo sp1 ( CurveData { curveIndex } ) -> do
-- Check endpoints first.
traverse_ ( dragSelectSplinePoint uniq ( PointIndex curveIndex PathPoint ) ) sp1
let
mbCurveDrag :: Maybe DragMoveSelect
mbCurveDrag = do
let
t :: Double
p :: 2
( t, p ) =
closestPointOnSegment @( T ( 2 ) )
c
( Segment ( coords sp0 ) ( coords $ fromNextPoint start sp1 ) )
guard ( inSelectionRange p )
pure $
ClickedOnCurve
{ dragStrokeUnique = uniq
, dragCurve = curveIndex
, dragCurveIndex = i
, dragCurveParameter = t
}
for_ mbCurveDrag Except.throwE
Bezier2To sp1 sp2 ( CurveData { curveIndex } ) -> do
-- Check endpoints first.
dragSelectSplinePoint uniq ( PointIndex curveIndex ( ControlPoint Bez2Cp ) ) sp1
traverse_ ( dragSelectSplinePoint uniq ( PointIndex curveIndex PathPoint ) ) sp2
let
mbCurveDrag :: Maybe DragMoveSelect
mbCurveDrag = do
let
bez :: Quadratic.Bezier ( 2 )
bez = Quadratic.Bezier ( coords sp0 ) ( coords sp1 ) ( coords $ fromNextPoint start sp2 )
sq_d :: Double
t :: Double
Min ( Arg sq_d (t, _) ) =
Quadratic.closestPoint @( T ( 2 ) ) bez c
guard ( t > 0 && t < 1 && sq_d * zoomFactor ^ ( 2 :: Int ) < 16 )
pure $
ClickedOnCurve
{ dragStrokeUnique = uniq
, dragCurve = curveIndex
, dragCurveIndex = i
, dragCurveParameter = t
}
for_ mbCurveDrag Except.throwE
Bezier3To sp1 sp2 sp3 ( CurveData { curveIndex } ) -> do
-- Check endpoints first.
dragSelectSplinePoint uniq ( PointIndex curveIndex ( ControlPoint Bez3Cp1 ) ) sp1
dragSelectSplinePoint uniq ( PointIndex curveIndex ( ControlPoint Bez3Cp2 ) ) sp2
traverse_ ( dragSelectSplinePoint uniq ( PointIndex curveIndex PathPoint ) ) sp3
let
mbCurveDrag :: Maybe DragMoveSelect
mbCurveDrag = do
let
bez :: Cubic.Bezier ( 2 )
bez = Cubic.Bezier ( coords sp0 ) ( coords sp1 ) ( coords sp2 ) ( coords $ fromNextPoint start sp3 )
sq_d :: Double
t :: Double
Min ( Arg sq_d (t, _) ) =
Cubic.closestPoint @( T ( 2 ) ) bez c
guard ( t > 0 && t < 1 && sq_d * zoomFactor ^ ( 2 :: Int ) < 16 )
pure $
ClickedOnCurve
{ dragStrokeUnique = uniq
, dragCurve = curveIndex
, dragCurveIndex = i
, dragCurveParameter = t
}
for_ mbCurveDrag Except.throwE
dragSelectSplinePoint :: Unique -> PointIndex -> PointData brushParams -> Except DragMoveSelect ()
dragSelectSplinePoint u ptIx pt =
when ( inSelectionRange ( coords pt ) ) do
let
drag :: DragMoveSelect
drag = ClickedOnPoint
{ dragPoint = ( u, ptIx )
, dragPointWasSelected =
elemStrokePoint u ptIx ( selectedPoints documentMetadata )
}
Except.throwE drag
addOrRemove :: SelectionMode
-> StrokePoints -- ^ items to add or remove
-> StrokePoints -- ^ base collection
-> StrokePoints
addOrRemove selMode new old =
case selMode of
New -> new
Add -> old <> new
Subtract -> old `differenceStrokePoints` new
-- | Updates the selected objects on a rectangular selection event.
selectRectangle :: SelectionMode -> 2 -> 2 -> Document -> Maybe ( Document, StrokePoints )
selectRectangle selMode ( 2 x0 y0 ) ( 2 x1 y1 ) doc@( Document { documentContent, documentMetadata } ) =
let
selPts =
Writer.execWriter $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
selectRect
in if noStrokePoints selPts && selMode /= New
then Nothing
else
let doc' = over ( field' @"documentMetadata" . field' @"selectedPoints" )
( addOrRemove selMode selPts )
doc
in Just ( doc', selPts )
where
xMin, xMax, yMin, yMax :: Double
( xMin, xMax ) = if x0 <= x1 then ( x0, x1 ) else ( x1, x0 )
( yMin, yMax ) = if y0 <= y1 then ( y0, y1 ) else ( y1, y0 )
selectRect :: Unique -> Stroke -> StrokeMetadata -> Writer StrokePoints UpdateStroke
selectRect strokeUnique ( Stroke { strokeSpline } ) ( StrokeMetadata { strokeVisible } ) = do
when strokeVisible $
Writer.mapWriter ( second $ \ is -> StrokePoints ( Map.singleton strokeUnique is ) ) $
bifoldSpline
( \ _ -> selectRectSplineCurve )
( selectRectSplinePoint FirstPoint )
strokeSpline
return PreserveStroke
selectRectSplineCurve :: SplineTypeI clo
2024-09-28 11:07:56 +00:00
=> Curve clo ( CurveData RealWorld ) ( PointData ptData )
-> Writer ( Set PointIndex ) ()
selectRectSplineCurve = \case
LineTo p1 ( CurveData { curveIndex } ) ->
traverse_ ( selectRectSplinePoint ( PointIndex curveIndex PathPoint ) ) p1
Bezier2To cp1 p2 ( CurveData { curveIndex } ) -> do
selectRectSplinePoint ( PointIndex curveIndex ( ControlPoint Bez2Cp ) ) cp1
traverse_ ( selectRectSplinePoint ( PointIndex curveIndex PathPoint ) ) p2
Bezier3To cp1 cp2 p3 ( CurveData { curveIndex } ) -> do
selectRectSplinePoint ( PointIndex curveIndex ( ControlPoint Bez3Cp1 ) ) cp1
selectRectSplinePoint ( PointIndex curveIndex ( ControlPoint Bez3Cp2 ) ) cp2
traverse_ ( selectRectSplinePoint ( PointIndex curveIndex PathPoint ) ) p3
selectRectSplinePoint :: PointIndex -> PointData brushParams -> Writer ( Set PointIndex ) ()
selectRectSplinePoint ptIx pt = do
let
x, y :: Double
2 x y = coords pt
selected :: Bool
selected =
x >= xMin && x <= xMax && y >= yMin && y <= yMax
when selected $
Writer.tell $ Set.singleton ptIx
-- | Translate all selected points by the given vector.
--
-- Returns the updated document, together with info about how many points were translated.
translateSelection :: T ( 2 ) -> Document -> Maybe ( Document, StrokePoints )
translateSelection t doc@( Document { documentContent, documentMetadata } ) =
let
( newStrokes, movedPts ) =
Writer.runWriter $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
updateStroke
in
if noStrokePoints movedPts
then Nothing
else Just ( set ( field' @"documentContent" . field' @"strokeHierarchy" ) newStrokes doc
, movedPts
)
where
selPts :: StrokePoints
selPts = selectedPoints documentMetadata
updateStroke :: Unique -> Stroke -> StrokeMetadata -> Writer StrokePoints UpdateStroke
updateStroke u stroke@( Stroke { strokeSpline } ) ( StrokeMetadata { strokeVisible, strokeLocked } ) = do
let strokeSelPts = fromMaybe Set.empty $ Map.lookup u ( strokePoints selPts )
firstPointSel = FirstPoint `Set.member` strokeSelPts
( spline', ( modPts, _ ) ) =
( `State.runState` ( mempty, False ) ) $
2024-09-28 11:07:56 +00:00
bitraverseSpline @_ @_ @( CurveData RealWorld ) @( PointData _ )
( \ _ -> updateSplineCurve firstPointSel strokeSelPts )
( \ pt -> do { ( NextPoint pt', _ ) <- updatePoint firstPointSel strokeSelPts FirstPoint ( NextPoint pt ) ; return pt' } )
strokeSpline
if strokeVisible && not strokeLocked && not ( null modPts )
then do
Writer.tell ( StrokePoints $ Map.singleton u modPts )
return $ UpdateStrokeTo ( stroke { strokeSpline = spline' } )
else
return PreserveStroke
updateSplineCurve
:: forall clo' brushParams
. SplineTypeI clo'
=> Bool
-> Set PointIndex
2024-09-28 11:07:56 +00:00
-> Curve clo' ( CurveData RealWorld ) ( PointData brushParams )
-> State ( Set PointIndex, Bool )
2024-09-28 11:07:56 +00:00
( Curve clo' ( CurveData RealWorld ) ( PointData brushParams ) )
updateSplineCurve startPtSel strokeSelPts = \case
LineTo p1 dat@( CurveData { curveIndex } ) -> do
( _, sel0 ) <- State.get
( p1', sel1 ) <- updatePoint startPtSel strokeSelPts ( PointIndex curveIndex PathPoint ) p1
let
2024-09-28 11:07:56 +00:00
dat' :: ( CurveData RealWorld )
dat'
| sel0 || sel1
= invalidateCache dat
| otherwise
= dat
State.modify' ( \ ( pts, _ ) -> ( pts, sel1 ) )
pure $ LineTo p1' dat'
Bezier2To p1 p2 dat@( CurveData { curveIndex } ) -> do
( _, sel0 ) <- State.get
( NextPoint p1', sel1 ) <- updatePoint startPtSel strokeSelPts ( PointIndex curveIndex ( ControlPoint Bez2Cp )) ( NextPoint p1 )
( p2', sel2 ) <- updatePoint startPtSel strokeSelPts ( PointIndex curveIndex PathPoint ) p2
let
2024-09-28 11:07:56 +00:00
dat' :: ( CurveData RealWorld )
dat'
| sel0 || sel1 || sel2
= invalidateCache dat
| otherwise
= dat
State.modify' ( \ ( pts, _ ) -> ( pts, sel2 ) )
pure $ Bezier2To p1' p2' dat'
Bezier3To p1 p2 p3 dat@( CurveData { curveIndex } ) -> do
( _, sel0 ) <- State.get
( NextPoint p1', sel1 ) <- updatePoint startPtSel strokeSelPts ( PointIndex curveIndex ( ControlPoint Bez3Cp1 )) ( NextPoint p1 )
( NextPoint p2', sel2 ) <- updatePoint startPtSel strokeSelPts ( PointIndex curveIndex ( ControlPoint Bez3Cp2 )) ( NextPoint p2 )
( p3', sel3 ) <- updatePoint startPtSel strokeSelPts ( PointIndex curveIndex PathPoint ) p3
let
2024-09-28 11:07:56 +00:00
dat' :: ( CurveData RealWorld )
dat'
| sel0 || sel1 || sel2 || sel3
= invalidateCache dat
| otherwise
= dat
State.modify' ( \ ( pts, _ ) -> ( pts, sel3 ) )
pure $ Bezier3To p1' p2' p3' dat'
updatePoint :: forall clo brushParams
. SplineTypeI clo
=> Bool
-> Set PointIndex
-> PointIndex
-> NextPoint clo ( PointData brushParams )
-> State ( Set PointIndex, Bool ) ( NextPoint clo ( PointData brushParams ), Bool )
updatePoint startPtSel strokeSelPts i nextPt = do
if i `Set.member` strokeSelPts
then do
nextPt' <- for nextPt $ \ pt -> do
State.modify' ( \ ( pts, _ ) -> ( Set.insert i pts, True ) )
return $ over _coords ( t ) pt
return ( nextPt', True )
else do
let endPtSel :: Bool
endPtSel = case ssplineType @clo of
SOpen -> False
SClosed -> case nextPt of
BackToStart -> startPtSel
State.modify' ( \ ( pts, _ ) -> ( pts, endPtSel ) )
return ( nextPt, endPtSel )
-- | Delete the selected points.
--
-- Returns the updated document, together with info about how many points
-- and strokes were deleted.
deleteSelected :: Document -> Maybe ( Document, StrokePoints, Set Unique )
deleteSelected doc@( Document { documentContent, documentMetadata } ) =
let
( newStrokes, ( delPts, delStrokes ) ) =
Writer.runWriter $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
updateStroke
in
if noStrokePoints delPts
then Nothing
else Just ( set ( field' @"documentContent" . field' @"strokeHierarchy" ) newStrokes $ doc
-- NB: not removing metadata about the layer from the LayerMetadata,
-- as it might be useful to keep it around if we undo the action.
, delPts
, delStrokes
)
where
updateStroke :: Unique -> Stroke -> StrokeMetadata -> Writer ( StrokePoints, Set Unique ) UpdateStroke
updateStroke u stroke@( Stroke { strokeSpline = oldSpline :: StrokeSpline clo brushParams } ) ( StrokeMetadata { strokeVisible, strokeLocked } )
| not strokeVisible || strokeLocked
= return PreserveStroke
| otherwise
= let
( mbSpline, delPts ) =
Writer.runWriter $
biwitherSpline
updateSplineCurve
( updateSplinePoint FirstPoint )
oldSpline
in if null delPts
then
return PreserveStroke
else case mbSpline of
Nothing -> do
Writer.tell ( StrokePoints $ Map.singleton u delPts, Set.singleton u )
return DeleteStroke
Just spline' -> do
Writer.tell ( StrokePoints $ Map.singleton u delPts, Set.empty )
return $ UpdateStrokeTo $ stroke { strokeSpline = spline' }
where
strokeSelPts = fromMaybe Set.empty $ Map.lookup u ( strokePoints $ selectedPoints documentMetadata )
updateSplinePoint
:: PointIndex
-> PointData brushParams
-> Writer ( Set PointIndex )
( Maybe ( PointData brushParams ) )
updateSplinePoint i pt =
if i `Set.member` strokeSelPts
then
do Writer.tell ( Set.singleton i )
return Nothing
else
return ( Just pt )
updateSplineCurve
:: forall clo' hasStart. SplineTypeI clo'
=> CurrentStart hasStart ( PointData brushParams )
2024-09-28 11:07:56 +00:00
-> Curve clo' ( CurveData RealWorld ) ( PointData brushParams )
-> Writer ( Set PointIndex )
2024-09-28 11:07:56 +00:00
( WitherResult hasStart clo' ( CurveData RealWorld ) ( PointData brushParams ) )
updateSplineCurve mbPrevPt crv = case crv of
LineTo p1 ( CurveData { curveIndex } ) ->
let i = PointIndex curveIndex PathPoint
in
case ssplineType @clo' of
SOpen
| i `Set.member` strokeSelPts
-> do
Writer.tell ( Set.singleton i )
pure Dismiss
| NextPoint pt <- p1
, NoStartFound <- mbPrevPt
-> pure ( UseStartPoint pt Nothing )
SClosed
| NoStartFound <- mbPrevPt
-> pure Dismiss
_ | CurrentStart wasOrigPt _ <- mbPrevPt
-> let crv' = if wasOrigPt
then crv
else crv { curveData = invalidateCache $ curveData crv }
in pure $ UseCurve crv'
Bezier2To _cp1 p2 dat@( CurveData { curveIndex } ) ->
let i = PointIndex curveIndex PathPoint
j = PointIndex curveIndex ( ControlPoint Bez2Cp )
in
case ssplineType @clo' of
SOpen
| i `Set.member` strokeSelPts
-> do
Writer.tell ( Set.fromList [ i, j ] )
pure Dismiss
| NextPoint pt <- p2
, NoStartFound <- mbPrevPt
-> do Writer.tell ( Set.singleton j )
pure ( UseStartPoint pt Nothing )
SClosed
| NoStartFound <- mbPrevPt
-> do Writer.tell ( Set.singleton j )
pure Dismiss
_ | CurrentStart wasOrigPt _ <- mbPrevPt
-> if j `Set.member` strokeSelPts
then do
Writer.tell ( Set.singleton j )
pure ( UseCurve ( LineTo p2 ( invalidateCache dat ) ) )
else
let crv' = if wasOrigPt
then crv
else crv { curveData = invalidateCache $ curveData crv }
in pure $ UseCurve crv'
Bezier3To cp1 cp2 p3 dat@( CurveData { curveIndex } ) ->
let i = PointIndex curveIndex PathPoint
j1 = PointIndex curveIndex ( ControlPoint Bez3Cp1 )
j2 = PointIndex curveIndex ( ControlPoint Bez3Cp2 )
in
case ssplineType @clo' of
SOpen
| i `Set.member` strokeSelPts
-> do
Writer.tell ( Set.fromList [ i, j1, j2 ] )
pure Dismiss
| NextPoint pt <- p3
, NoStartFound <- mbPrevPt
-> do
Writer.tell ( Set.fromList [ j1, j2 ] )
pure ( UseStartPoint pt Nothing )
SClosed
| NoStartFound <- mbPrevPt
-> do
Writer.tell ( Set.fromList [ j1, j2 ] )
pure Dismiss
_ | CurrentStart wasOrigPt _ <- mbPrevPt
-> case ( j1 `Set.member` strokeSelPts
, j2 `Set.member` strokeSelPts ) of
( False, False ) ->
let crv' = if wasOrigPt
then crv
else crv { curveData = invalidateCache $ curveData crv }
in pure $ UseCurve crv'
( False, _ ) -> do
Writer.tell ( Set.singleton j2 )
pure ( UseCurve $ Bezier2To cp1 p3 ( invalidateCache dat ) )
( _, False ) -> do
Writer.tell ( Set.singleton j1 )
pure ( UseCurve $ Bezier2To cp2 p3 ( invalidateCache dat ) )
_ -> do
Writer.tell ( Set.fromList [ j1, j2 ] )
pure ( UseCurve $ LineTo p3 ( invalidateCache dat ) )
-- | Perform a drag move action on a document.
dragUpdate :: 2 -> 2 -> DragMoveSelect -> Bool -> Document -> Maybe ( Document, StrokePoints )
dragUpdate p0 p ( ClickedOnPoint {} ) _ doc = translateSelection ( p0 --> p ) doc
dragUpdate _ p ( ClickedOnCurve { dragStrokeUnique, dragCurveIndex, dragCurveParameter } ) alternateMode
doc@( Document { documentContent, documentMetadata } ) =
let
( newStrokes, movedPts ) =
Writer.runWriter $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
updateStroke
in
if noStrokePoints movedPts
then Nothing
else Just ( set ( field' @"documentContent" . field' @"strokeHierarchy" ) newStrokes doc
, movedPts
)
where
updateStroke :: Unique -> Stroke -> StrokeMetadata -> Writer StrokePoints UpdateStroke
updateStroke u stroke@( Stroke { strokeSpline = oldSpline :: StrokeSpline clo pointParams } ) ( StrokeMetadata { strokeVisible, strokeLocked } )
| not strokeVisible || strokeLocked || u /= dragStrokeUnique
= return PreserveStroke
| otherwise
= let
( spline', movedPts ) =
Writer.runWriter
$ fmap ( adjustSplineType @clo )
$ updateSplineCurves
$ adjustSplineType @Open oldSpline
in if null movedPts
then return PreserveStroke
else do
Writer.tell ( StrokePoints $ Map.singleton u movedPts )
return $ UpdateStrokeTo $ stroke { strokeSpline = spline' }
where
updateSplineCurves
:: StrokeSpline Open pointParams
-> Writer ( Set PointIndex ) ( StrokeSpline Open pointParams )
updateSplineCurves spline =
case splitSplineAt dragCurveIndex spline of
( _ , Spline { splineCurves = OpenCurves Seq.Empty } ) -> pure spline
( bef, Spline { splineStart, splineCurves = OpenCurves ( curve Seq.:<| next ) } ) -> do
curve' <- updateCurve ( lastPoint bef ) curve
pure ( bef <> Spline { splineStart, splineCurves = OpenCurves $ curve' Seq.:<| next } )
updateCurve
:: PointData pointParams
2024-09-28 11:07:56 +00:00
-> Curve Open ( CurveData RealWorld ) ( PointData pointParams )
-> Writer ( Set PointIndex )
2024-09-28 11:07:56 +00:00
( Curve Open ( CurveData RealWorld ) ( PointData pointParams ) )
updateCurve sp0 curve = case curve of
LineTo ( NextPoint sp1 ) dat@( CurveData { curveIndex } ) -> do
let
bez2 :: Quadratic.Bezier ( PointData pointParams )
bez2 = Quadratic.Bezier sp0 ( lerp @( DiffPointData ( T pointParams ) ) dragCurveParameter sp0 sp1 ) sp1
Writer.tell ( Set.singleton ( PointIndex curveIndex PathPoint ) )
pure $
if alternateMode
then quadraticDragCurve dat bez2
else cubicDragCurve dat ( Cubic.fromQuadratic @( DiffPointData ( T pointParams ) ) bez2 )
Bezier2To sp1 ( NextPoint sp2 ) dat@( CurveData { curveIndex } ) -> do
let
bez2 :: Quadratic.Bezier ( PointData pointParams )
bez2 = Quadratic.Bezier sp0 sp1 sp2
if alternateMode
then do
Writer.tell $
Set.fromList
[ PointIndex curveIndex PathPoint
, PointIndex curveIndex ( ControlPoint Bez2Cp )
, PointIndex curveIndex ( ControlPoint Bez3Cp1 )
, PointIndex curveIndex ( ControlPoint Bez3Cp2 )
]
pure $ cubicDragCurve dat $ Cubic.fromQuadratic @( DiffPointData ( T pointParams ) ) bez2
else do
Writer.tell $
Set.fromList
[ PointIndex curveIndex PathPoint
, PointIndex curveIndex ( ControlPoint Bez2Cp )
]
pure $ quadraticDragCurve dat ( Quadratic.Bezier sp0 sp1 sp2 )
Bezier3To sp1 sp2 ( NextPoint sp3 ) dat@( CurveData { curveIndex } ) -> do
let
bez3 :: Cubic.Bezier ( PointData pointParams )
bez3 = Cubic.Bezier sp0 sp1 sp2 sp3
if alternateMode
then do
Writer.tell $
Set.fromList
[ PointIndex curveIndex PathPoint
, PointIndex curveIndex ( ControlPoint Bez2Cp )
, PointIndex curveIndex ( ControlPoint Bez3Cp1 )
, PointIndex curveIndex ( ControlPoint Bez3Cp2 )
]
pure $ quadraticDragCurve dat
( Quadratic.Bezier
sp0
( Cubic.bezier @( DiffPointData ( T pointParams ) ) bez3 dragCurveParameter )
sp3
)
else do
Writer.tell $
Set.fromList
[ PointIndex curveIndex PathPoint
, PointIndex curveIndex ( ControlPoint Bez3Cp1 )
, PointIndex curveIndex ( ControlPoint Bez3Cp2 )
]
pure $ cubicDragCurve dat bez3
where
quadraticDragCurve
2024-09-28 11:07:56 +00:00
:: ( CurveData RealWorld )
-> Quadratic.Bezier ( PointData pointParams )
2024-09-28 11:07:56 +00:00
-> Curve Open ( CurveData RealWorld ) ( PointData pointParams )
quadraticDragCurve dat ( Quadratic.Bezier { Quadratic.p1 = sp1, Quadratic.p2 = sp2 } ) =
let
cp :: 2
Quadratic.Bezier { Quadratic.p1 = cp } =
Quadratic.interpolate @( T ( 2 ) ) ( coords sp0 ) ( coords sp2 ) dragCurveParameter p
in Bezier2To ( set _coords cp sp1 ) ( NextPoint sp2 ) ( invalidateCache dat )
cubicDragCurve
2024-09-28 11:07:56 +00:00
:: ( CurveData RealWorld )
-> Cubic.Bezier ( PointData pointParams )
2024-09-28 11:07:56 +00:00
-> Curve Open ( CurveData RealWorld ) ( PointData pointParams )
cubicDragCurve dat ( Cubic.Bezier { Cubic.p1 = sp1, Cubic.p2 = sp2, Cubic.p3 = sp3 } ) =
let
cp1, cp2 :: 2
Cubic.Bezier { Cubic.p1 = cp1, Cubic.p2 = cp2 } =
Cubic.drag @( T ( 2 ) )
( Cubic.Bezier ( coords sp0 ) ( coords sp1 ) ( coords sp2 ) ( coords sp3 ) )
dragCurveParameter
p
in Bezier3To ( set _coords cp1 sp1 ) ( set _coords cp2 sp2 ) ( NextPoint sp3 ) ( invalidateCache dat )
--------------------------------------------------------------------------------
-- Brush widget
data BrushWidgetActionState
= BrushWidgetActionState
{ brushWidgetAction :: !Brush.WidgetAction
, brushWidgetStrokeUnique :: !Unique
, brushWidgetPointIndex :: !PointIndex
, brushWidgetPointBeingMoved :: !( T ( 2 ) )
}
deriving stock ( Eq, Show )
-- | Apply a brush widget action, e.g. rotating or scaling the brush at a particular stroke point.
applyBrushWidgetAction :: Bool -> 2 -> Maybe BrushWidgetActionState -> Document -> Maybe ( Document, BrushWidgetActionState )
applyBrushWidgetAction pressingCtrl c mbPrevAction doc@( Document { documentContent, documentMetadata } ) =
let
( newStrokes, ( mbAct, _ ) ) =
( `State.runState` ( Nothing, False ) ) $
forStrokeHierarchy
( layerMetadata documentMetadata )
( strokeHierarchy documentContent )
updateStroke
in mbAct <&> \ act ->
( set ( field' @"documentContent" . field' @"strokeHierarchy" ) newStrokes doc
, act
)
where
zoom = documentZoom $ documentMetadata
updateStroke :: Unique -> Stroke -> StrokeMetadata -> State ( Maybe BrushWidgetActionState, Bool ) UpdateStroke
updateStroke u stroke@( Stroke { strokeBrush, strokeSpline = oldSpline :: StrokeSpline _clo ( Record pointFields ) } ) ( StrokeMetadata { strokeVisible, strokeLocked } )
| strokeVisible
, not strokeLocked
-- If we have already started a widget action, only continue an action
-- for the stroke with the correct unique.
, case mbPrevAction of { Just act -> brushWidgetStrokeUnique act == u; Nothing -> True }
-- Don't touch strokes without brushes.
, Just ( brush@( NamedBrush {} ) :: NamedBrush brushFields ) <- strokeBrush
= case intersect @pointFields @brushFields of
Intersection { inject1 = injectUsedParams, inject2 = updateBrushParams, project1 = ptParamsToUsedParams, project2 = brushParamsToUsedParams } -> do
let embedUsedParams = updateBrushParams ( MkR $ defaultParams $ brushFunction brush )
toBrushParams = embedUsedParams . ptParamsToUsedParams
updatePointParams brushParams' ptParams = injectUsedParams ptParams ( brushParamsToUsedParams brushParams' )
spline' <-
bitraverseSpline
( updateSplineCurve ( splineStart oldSpline ) brush toBrushParams updatePointParams u )
( updateSplinePoint brush toBrushParams updatePointParams u FirstPoint )
oldSpline
( mbAct, _ ) <- State.get
case mbAct of
Nothing -> return PreserveStroke
Just {} -> return $ UpdateStrokeTo $ stroke { strokeSpline = spline' }
| otherwise
= return PreserveStroke
updateSplineCurve
:: forall clo' pointParams brushFields
. ( SplineTypeI clo', Traversable ( NextPoint clo' ) )
=> PointData pointParams
-> NamedBrush brushFields
-> ( pointParams -> Record brushFields )
-> ( Record brushFields -> pointParams -> pointParams )
-> Unique
2024-09-28 11:07:56 +00:00
-> PointData pointParams -> Curve clo' ( CurveData RealWorld ) ( PointData pointParams )
-> State ( Maybe BrushWidgetActionState, Bool )
2024-09-28 11:07:56 +00:00
( Curve clo' ( CurveData RealWorld ) ( PointData pointParams ) )
updateSplineCurve _start brush toBrushParams updatePointParams uniq _sp0 curve = do
( mbAct, prevCurveAct ) <- State.get
-- We can only perform a brush widget update if:
-- - we aren't already updating another point,
-- - either:
-- - we are starting a new operation, or
-- - we are continuing an operation, and we have the right curve index
let canAct
| isNothing mbAct
= case mbPrevAction of
Just prevAct -> case ssplineType @clo' of
SClosed -> pointCurve ( brushWidgetPointIndex prevAct ) == crvIx
SOpen -> pointCurve ( brushWidgetPointIndex prevAct ) == crvIx
Nothing -> True
| otherwise
= False
if canAct
then do
case curve of
line@( LineTo sp1 dat ) -> do
sp1' <- traverse ( updateSplinePoint brush toBrushParams updatePointParams uniq ( PointIndex crvIx PathPoint ) ) sp1
pure ( line { curveEnd = sp1', curveData = invalidateCache dat } )
bez2@( Bezier2To sp1 sp2 dat ) -> do
sp1' <- updateSplinePoint brush toBrushParams updatePointParams uniq ( PointIndex crvIx ( ControlPoint Bez2Cp ) ) sp1
sp2' <- traverse ( updateSplinePoint brush toBrushParams updatePointParams uniq ( PointIndex crvIx PathPoint ) ) sp2
pure ( bez2 { controlPoint = sp1', curveEnd = sp2', curveData = invalidateCache dat } )
bez3@( Bezier3To sp1 sp2 sp3 dat ) -> do
sp1' <- updateSplinePoint brush toBrushParams updatePointParams uniq ( PointIndex crvIx ( ControlPoint Bez3Cp1 ) ) sp1
sp2' <- updateSplinePoint brush toBrushParams updatePointParams uniq ( PointIndex crvIx ( ControlPoint Bez3Cp2 ) ) sp2
sp3' <- traverse ( updateSplinePoint brush toBrushParams updatePointParams uniq ( PointIndex crvIx PathPoint ) ) sp3
pure ( bez3 { controlPoint1 = sp1', controlPoint2 = sp2', curveEnd = sp3', curveData = invalidateCache dat } )
else do
State.put ( mbAct, False )
let curve' = if prevCurveAct
then curve { curveData = invalidateCache $ curveData curve }
else curve
return curve'
where
crvIx = curveIndex ( curveData curve )
inSelectionRange :: 2 -> T ( 2 ) -> Bool
inSelectionRange p cp =
inPointClickRange zoom c ( cp p )
lineInSelectionRange :: 2 -> Segment ( T ( 2 ) ) -> Bool
lineInSelectionRange p seg =
case closestPointOnSegment @( T ( 2 ) ) c ( ( \ q -> ( q p ) ) <$> seg ) of
( _, q ) ->
inPointClickRange zoom c q
updateSplinePoint
:: forall pointParams brushFields
. NamedBrush brushFields
-> ( pointParams -> Record brushFields )
-> ( Record brushFields -> pointParams -> pointParams )
-> Unique -> PointIndex
-> PointData pointParams
-> State ( Maybe BrushWidgetActionState, Bool )
( PointData pointParams )
updateSplinePoint brush toBrushParams updatePointParams uniq j pt = do
let
currentBrushParams :: Record brushFields
currentBrushParams = toBrushParams ( brushParams pt )
brushWidgetElts :: Brush.WidgetElements
brushWidgetElts = Brush.widgetElements ( brushWidget brush ) currentBrushParams
newBrushWidgetAction :: Maybe BrushWidgetActionState
( newBrushWidgetAction, newBrushParams ) = case mbPrevAction of
-- Continue the current brush widget action.
Just prevAction@( BrushWidgetActionState
{ brushWidgetPointBeingMoved = oldPt
, brushWidgetPointIndex = j'
, brushWidgetAction = act }) ->
if j /= j'
-- If we have already started a widget action, only continue an action
-- at the point with the correct index in the stroke.
then ( Nothing, currentBrushParams )
else
let newPt = pointCoords pt --> c
newParams =
Brush.widgetUpdate ( brushWidget brush ) act
( oldPt, newPt )
currentBrushParams
in ( Just $ prevAction { brushWidgetPointBeingMoved = newPt }, newParams )
Nothing ->
-- See if we can start a new brush widget action.
case listToMaybe $ filter ( inSelectionRange $ pointCoords pt ) ( Brush.widgetPoints brushWidgetElts ) of
Just cp ->
let newAction =
BrushWidgetActionState
{ brushWidgetPointBeingMoved = cp
, brushWidgetStrokeUnique = uniq
, brushWidgetPointIndex = j
, brushWidgetAction = case brushWidget brush of
Brush.SquareWidget -> Brush.ScaleAction Brush.ScaleXY
Brush.RotatableRectangleWidget ->
if pressingCtrl
then Brush.RotateAction
else Brush.ScaleAction Brush.ScaleXY
}
in ( Just newAction, currentBrushParams )
Nothing -> ( Nothing, currentBrushParams )
-- TODO: handle clicking on an edge.
-- case listToMaybe $ filter ( lineInSelectionRange $ pointCoords pt ) ( Brush.widgetLines brushWidgetElts ) of
-- Just ln -> error "todo"
-- Nothing -> Nothing
case newBrushWidgetAction of
Just a -> State.put ( Just a, True )
_ -> State.modify' ( \ ( a, _ ) -> ( a, False ) )
pure ( set ( field' @"brushParams" ) ( updatePointParams newBrushParams ( brushParams pt ) ) pt )