mirror of
https://gitlab.com/sheaf/metabrush.git
synced 2024-11-27 17:34:08 +00:00
basic rendering, mousewheel scrolling
This commit is contained in:
parent
8e051c0f14
commit
9fc7b8acb3
|
@ -28,6 +28,8 @@ common common
|
||||||
build-depends:
|
build-depends:
|
||||||
base
|
base
|
||||||
>= 4.13 && < 4.16
|
>= 4.13 && < 4.16
|
||||||
|
, acts
|
||||||
|
^>= 0.3.1.0
|
||||||
|
|
||||||
default-language:
|
default-language:
|
||||||
Haskell2010
|
Haskell2010
|
||||||
|
@ -61,9 +63,7 @@ library
|
||||||
, Math.Vector2D
|
, Math.Vector2D
|
||||||
|
|
||||||
build-depends:
|
build-depends:
|
||||||
acts
|
generic-data
|
||||||
^>= 0.3.1.0
|
|
||||||
, generic-data
|
|
||||||
>= 0.8.0.0 && < 0.8.4.0
|
>= 0.8.0.0 && < 0.8.4.0
|
||||||
, groups
|
, groups
|
||||||
^>= 0.4.1.0
|
^>= 0.4.1.0
|
||||||
|
@ -92,8 +92,8 @@ executable MetaBrush
|
||||||
, MetaBrush.Asset.WindowIcons
|
, MetaBrush.Asset.WindowIcons
|
||||||
, MetaBrush.Document
|
, MetaBrush.Document
|
||||||
, MetaBrush.Event
|
, MetaBrush.Event
|
||||||
|
, MetaBrush.Render.Document
|
||||||
, MetaBrush.Render.Util
|
, MetaBrush.Render.Util
|
||||||
, MetaBrush.Stroke
|
|
||||||
, MetaBrush.UI.Menu
|
, MetaBrush.UI.Menu
|
||||||
, Paths_MetaBrush
|
, Paths_MetaBrush
|
||||||
|
|
||||||
|
@ -105,8 +105,12 @@ executable MetaBrush
|
||||||
|
|
||||||
build-depends:
|
build-depends:
|
||||||
MetaBrush
|
MetaBrush
|
||||||
|
, containers
|
||||||
|
>= 0.6.0.1 && < 0.6.4
|
||||||
, directory
|
, directory
|
||||||
>= 1.3.4.0 && < 1.4
|
>= 1.3.4.0 && < 1.4
|
||||||
|
--, fingertree
|
||||||
|
-- >= 0.1.4.2 && < 0.2
|
||||||
, generic-lens
|
, generic-lens
|
||||||
>= 1.2.0.1 && < 2.0
|
>= 1.2.0.1 && < 2.0
|
||||||
, gi-gdk
|
, gi-gdk
|
||||||
|
@ -123,5 +127,7 @@ executable MetaBrush
|
||||||
^>= 0.0.1
|
^>= 0.0.1
|
||||||
, haskell-gi-base
|
, haskell-gi-base
|
||||||
^>= 0.24
|
^>= 0.24
|
||||||
|
, stm
|
||||||
|
^>= 2.5.0.0
|
||||||
, text
|
, text
|
||||||
^>= 1.2.3.1 && < 1.2.5
|
^>= 1.2.3.1 && < 1.2.5
|
||||||
|
|
271
app/Main.hs
271
app/Main.hs
|
@ -2,8 +2,10 @@
|
||||||
{-# LANGUAGE ConstraintKinds #-}
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
{-# LANGUAGE GADTs #-}
|
{-# LANGUAGE GADTs #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE NegativeLiterals #-}
|
{-# LANGUAGE NegativeLiterals #-}
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
|
@ -13,14 +15,28 @@ module Main
|
||||||
|
|
||||||
-- base
|
-- base
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
( void )
|
( void, unless )
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
( for_ )
|
( for_ )
|
||||||
import Data.Int
|
import Data.Int
|
||||||
( Int32 )
|
( Int32 )
|
||||||
|
import Data.Word
|
||||||
|
( Word32 )
|
||||||
import System.Exit
|
import System.Exit
|
||||||
( exitSuccess )
|
( exitSuccess )
|
||||||
|
|
||||||
|
-- acts
|
||||||
|
import Data.Act
|
||||||
|
( Act
|
||||||
|
( (•) )
|
||||||
|
)
|
||||||
|
|
||||||
|
-- containers
|
||||||
|
import Data.IntMap.Strict
|
||||||
|
( IntMap )
|
||||||
|
import qualified Data.IntMap.Strict as IntMap
|
||||||
|
( fromList, lookup, insert, traverseWithKey )
|
||||||
|
|
||||||
-- directory
|
-- directory
|
||||||
import qualified System.Directory as Directory
|
import qualified System.Directory as Directory
|
||||||
( canonicalizePath )
|
( canonicalizePath )
|
||||||
|
@ -38,11 +54,21 @@ import qualified GI.Gdk as GDK
|
||||||
-- gi-gtk
|
-- gi-gtk
|
||||||
import qualified GI.Gtk as GTK
|
import qualified GI.Gtk as GTK
|
||||||
|
|
||||||
|
-- stm
|
||||||
|
import qualified Control.Concurrent.STM as STM
|
||||||
|
( atomically )
|
||||||
|
import qualified Control.Concurrent.STM.TVar as STM
|
||||||
|
( newTVarIO, writeTVar, readTVarIO )
|
||||||
|
|
||||||
-- text
|
-- text
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
( pack )
|
( pack )
|
||||||
|
|
||||||
-- MetaBrush
|
-- MetaBrush
|
||||||
|
import Math.Module
|
||||||
|
( (*^) )
|
||||||
|
import Math.Vector2D
|
||||||
|
( Point2D(..), Vector2D(..) )
|
||||||
import MetaBrush.Asset.Colours
|
import MetaBrush.Asset.Colours
|
||||||
( getColours )
|
( getColours )
|
||||||
import MetaBrush.Asset.Cursor
|
import MetaBrush.Asset.Cursor
|
||||||
|
@ -55,8 +81,18 @@ import MetaBrush.Asset.Tools
|
||||||
( drawBrush, drawMeta, drawPath, drawPen )
|
( drawBrush, drawMeta, drawPath, drawPen )
|
||||||
import MetaBrush.Asset.WindowIcons
|
import MetaBrush.Asset.WindowIcons
|
||||||
( drawMinimise, drawRestoreDown, drawMaximise, drawClose )
|
( drawMinimise, drawRestoreDown, drawMaximise, drawClose )
|
||||||
|
import MetaBrush.Document
|
||||||
|
( Document(..)
|
||||||
|
, AABB(..)
|
||||||
|
, Stroke(..)
|
||||||
|
)
|
||||||
import MetaBrush.Event
|
import MetaBrush.Event
|
||||||
( handleKeyboardPressEvent, handleKeyboardReleaseEvent )
|
( handleKeyboardPressEvent, handleKeyboardReleaseEvent
|
||||||
|
, pattern Control_L, pattern Control_R
|
||||||
|
, pattern Shift_L, pattern Shift_R
|
||||||
|
)
|
||||||
|
import MetaBrush.Render.Document
|
||||||
|
( renderDocument )
|
||||||
import MetaBrush.Render.Util
|
import MetaBrush.Render.Util
|
||||||
( widgetAddClass, widgetAddClasses )
|
( widgetAddClass, widgetAddClasses )
|
||||||
import MetaBrush.UI.Menu
|
import MetaBrush.UI.Menu
|
||||||
|
@ -66,9 +102,41 @@ import qualified Paths_MetaBrush as Cabal
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
testDocuments :: IntMap Document
|
||||||
|
testDocuments = IntMap.fromList
|
||||||
|
$ zip [0..]
|
||||||
|
[ Document
|
||||||
|
{ displayName = "Document 1"
|
||||||
|
, filePath = Nothing
|
||||||
|
, unsavedChanges = False
|
||||||
|
, strokes = [ Stroke [ Point2D 10 10, Point2D 30 30, Point2D 40 70 ] ]
|
||||||
|
, bounds = AABB ( Point2D 0 0 ) ( Point2D 100 100 )
|
||||||
|
, viewportCenter = Point2D 50 50
|
||||||
|
, zoomFactor = 1
|
||||||
|
}
|
||||||
|
, Document
|
||||||
|
{ displayName = "Document 2"
|
||||||
|
, filePath = Nothing
|
||||||
|
, unsavedChanges = True
|
||||||
|
, strokes = [ Stroke [ Point2D 0 0, Point2D 10 10, Point2D 20 20 ] ]
|
||||||
|
, bounds = AABB ( Point2D 0 0 ) ( Point2D 50 50 )
|
||||||
|
, viewportCenter = Point2D 10 10
|
||||||
|
, zoomFactor = 0.25
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
|
|
||||||
|
---------------------------------------------------------
|
||||||
|
-- Initialise state
|
||||||
|
|
||||||
|
activeDocumentTVar <- STM.newTVarIO @( Maybe Int ) Nothing
|
||||||
|
openDocumentsTVar <- STM.newTVarIO @( IntMap Document ) testDocuments
|
||||||
|
pressedKeysTVar <- STM.newTVarIO @[ Word32 ] []
|
||||||
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
-- Initialise GTK
|
-- Initialise GTK
|
||||||
|
|
||||||
|
@ -84,7 +152,7 @@ main = do
|
||||||
windowWidgetPath <- GTK.widgetGetPath window
|
windowWidgetPath <- GTK.widgetGetPath window
|
||||||
widgetAddClass window "window"
|
widgetAddClass window "window"
|
||||||
GTK.setWindowResizable window True
|
GTK.setWindowResizable window True
|
||||||
GTK.setWindowDecorated window True
|
GTK.setWindowDecorated window False
|
||||||
GTK.setWindowTitle window "MetaBrush"
|
GTK.setWindowTitle window "MetaBrush"
|
||||||
GTK.windowSetDefaultSize window 800 600
|
GTK.windowSetDefaultSize window 800 600
|
||||||
|
|
||||||
|
@ -219,7 +287,6 @@ main = do
|
||||||
$ Cairo.renderWithContext
|
$ Cairo.renderWithContext
|
||||||
( drawClose colours )
|
( drawClose colours )
|
||||||
|
|
||||||
|
|
||||||
for_ [ minimiseButton, fullscreenButton, closeButton ] \ button -> do
|
for_ [ minimiseButton, fullscreenButton, closeButton ] \ button -> do
|
||||||
widgetAddClass button "windowIcon"
|
widgetAddClass button "windowIcon"
|
||||||
|
|
||||||
|
@ -293,60 +360,6 @@ main = do
|
||||||
$ Cairo.renderWithContext
|
$ Cairo.renderWithContext
|
||||||
( drawMeta colours )
|
( drawMeta colours )
|
||||||
|
|
||||||
---------------------------------------------------------
|
|
||||||
-- File bar
|
|
||||||
|
|
||||||
widgetAddClass fileBar "fileBar"
|
|
||||||
|
|
||||||
fileTabs <- GTK.boxNew GTK.OrientationHorizontal 0
|
|
||||||
GTK.containerAdd fileBar fileTabs
|
|
||||||
widgetAddClasses fileTabs [ "fileBar", "plain", "text" ]
|
|
||||||
|
|
||||||
fileBarPhantomRadioButton <- GTK.radioButtonNew ( [] @GTK.RadioButton )
|
|
||||||
|
|
||||||
for_ [ 1 .. 12 ] \ i -> do
|
|
||||||
-- File tab elements.
|
|
||||||
pgButton <- GTK.radioButtonNewWithLabelFromWidget ( Just fileBarPhantomRadioButton ) ( "New Document (" <> Text.pack ( show i ) <> ")" )
|
|
||||||
GTK.toggleButtonSetMode pgButton False -- don't display radio indicator
|
|
||||||
closeFileButton <- GTK.buttonNewWithLabel "x"
|
|
||||||
|
|
||||||
-- Create box for file tab elements.
|
|
||||||
tab <- GTK.boxNew GTK.OrientationHorizontal 0
|
|
||||||
widgetAddClasses tab [ "fileBarTab" ]
|
|
||||||
GTK.boxPackStart fileTabs tab False False 0
|
|
||||||
GTK.boxPackStart tab pgButton True True 0
|
|
||||||
GTK.boxPackStart tab closeFileButton False False 0
|
|
||||||
|
|
||||||
widgetAddClasses pgButton [ "fileBarTabButton" ]
|
|
||||||
widgetAddClasses closeFileButton [ "fileBarCloseButton" ]
|
|
||||||
|
|
||||||
-- Make both file tab elements activate styling on the whole tab
|
|
||||||
-- (e.g. hovering over the close file button should highlight the whole tab).
|
|
||||||
void $ GTK.onButtonClicked pgButton do
|
|
||||||
isActive <- GTK.toggleButtonGetActive pgButton
|
|
||||||
flags <- GTK.widgetGetStateFlags tab
|
|
||||||
if isActive
|
|
||||||
then GTK.widgetSetStateFlags tab ( GTK.StateFlagsActive : flags ) True
|
|
||||||
else GTK.widgetSetStateFlags tab ( filter ( /= GTK.StateFlagsActive ) flags ) True
|
|
||||||
|
|
||||||
{-
|
|
||||||
void $ GTK.onButtonClicked closeFileButton do
|
|
||||||
closeFileDialog ...
|
|
||||||
-}
|
|
||||||
|
|
||||||
for_ @_ @_ @_ @() [ Exists @GTK.IsWidget pgButton, Exists @GTK.IsWidget closeFileButton ] \ ( Exists button ) -> do
|
|
||||||
void $ GTK.onWidgetEnterNotifyEvent button \ _ -> do
|
|
||||||
flags <- GTK.widgetGetStateFlags tab
|
|
||||||
GTK.widgetSetStateFlags tab ( GTK.StateFlagsPrelight : flags ) True
|
|
||||||
pure False
|
|
||||||
void $ GTK.onWidgetLeaveNotifyEvent button \ _ -> do
|
|
||||||
flags <- GTK.widgetGetStateFlags tab
|
|
||||||
GTK.widgetSetStateFlags tab ( filter ( /= GTK.StateFlagsPrelight ) flags ) True
|
|
||||||
pure False
|
|
||||||
|
|
||||||
GTK.scrolledWindowSetPolicy fileBar GTK.PolicyTypeAutomatic GTK.PolicyTypeNever
|
|
||||||
GTK.scrolledWindowSetOverlayScrolling fileBar True
|
|
||||||
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
-- Main viewport
|
-- Main viewport
|
||||||
|
|
||||||
|
@ -362,6 +375,9 @@ main = do
|
||||||
GTK.gridAttach viewportGrid rvTopRuler 1 0 1 1
|
GTK.gridAttach viewportGrid rvTopRuler 1 0 1 1
|
||||||
GTK.gridAttach viewportGrid viewportOverlay 1 1 1 1
|
GTK.gridAttach viewportGrid viewportOverlay 1 1 1 1
|
||||||
|
|
||||||
|
----------
|
||||||
|
-- Rulers
|
||||||
|
|
||||||
rulerCorner <- GTK.boxNew GTK.OrientationVertical 0
|
rulerCorner <- GTK.boxNew GTK.OrientationVertical 0
|
||||||
leftRuler <- GTK.boxNew GTK.OrientationVertical 0
|
leftRuler <- GTK.boxNew GTK.OrientationVertical 0
|
||||||
topRuler <- GTK.boxNew GTK.OrientationHorizontal 0
|
topRuler <- GTK.boxNew GTK.OrientationHorizontal 0
|
||||||
|
@ -403,8 +419,12 @@ main = do
|
||||||
viewportArea <- GTK.drawingAreaNew
|
viewportArea <- GTK.drawingAreaNew
|
||||||
GTK.setContainerChild viewportOverlay viewportArea
|
GTK.setContainerChild viewportOverlay viewportArea
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
-- Viewport scrolling
|
||||||
|
|
||||||
viewportScrollbarGrid <- GTK.gridNew
|
viewportScrollbarGrid <- GTK.gridNew
|
||||||
GTK.overlayAddOverlay viewportOverlay viewportScrollbarGrid
|
GTK.overlayAddOverlay viewportOverlay viewportScrollbarGrid
|
||||||
|
GTK.overlaySetOverlayPassThrough viewportOverlay viewportScrollbarGrid True
|
||||||
|
|
||||||
viewportHScrollbar <- GTK.scrollbarNew GTK.OrientationHorizontal ( Nothing @GTK.Adjustment )
|
viewportHScrollbar <- GTK.scrollbarNew GTK.OrientationHorizontal ( Nothing @GTK.Adjustment )
|
||||||
viewportVScrollbar <- GTK.scrollbarNew GTK.OrientationVertical ( Nothing @GTK.Adjustment )
|
viewportVScrollbar <- GTK.scrollbarNew GTK.OrientationVertical ( Nothing @GTK.Adjustment )
|
||||||
|
@ -417,6 +437,129 @@ main = do
|
||||||
widgetAddClass viewportHScrollbar "viewportScrollbar"
|
widgetAddClass viewportHScrollbar "viewportScrollbar"
|
||||||
widgetAddClass viewportVScrollbar "viewportScrollbar"
|
widgetAddClass viewportVScrollbar "viewportScrollbar"
|
||||||
|
|
||||||
|
void $ GTK.onWidgetScrollEvent viewportArea \ scrollEvent -> do
|
||||||
|
|
||||||
|
dx <- GDK.getEventScrollDeltaX scrollEvent
|
||||||
|
dy <- GDK.getEventScrollDeltaY scrollEvent
|
||||||
|
--GDK.getEventScrollDirection scrollEvent
|
||||||
|
--GDK.getEventScrollType scrollEvent
|
||||||
|
--GDK.getEventScrollX scrollEvent
|
||||||
|
--GDK.getEventScrollY scrollEvent
|
||||||
|
|
||||||
|
unless ( dx == 0 && dy == 0 ) do
|
||||||
|
mbActiveDoc <- STM.readTVarIO activeDocumentTVar
|
||||||
|
for_ mbActiveDoc \ i -> do
|
||||||
|
docs <- STM.readTVarIO openDocumentsTVar
|
||||||
|
for_ ( IntMap.lookup i docs ) \ ( doc@(Document { viewportCenter, zoomFactor } ) ) -> do
|
||||||
|
pressedKeys <- STM.readTVarIO pressedKeysTVar
|
||||||
|
let
|
||||||
|
newDoc :: Document
|
||||||
|
newDoc
|
||||||
|
-- Zooming using 'Control'.
|
||||||
|
| any ( \ key -> key == Control_L || key == Control_R ) pressedKeys
|
||||||
|
= let
|
||||||
|
newZoomFactor :: Double
|
||||||
|
newZoomFactor
|
||||||
|
| dy > 0
|
||||||
|
= max 0.0078125 ( zoomFactor / 2 )
|
||||||
|
| otherwise
|
||||||
|
= zoomFactor * 2
|
||||||
|
in doc { zoomFactor = newZoomFactor }
|
||||||
|
-- Vertical scrolling turned into horizontal scrolling using 'Shift'.
|
||||||
|
| dx == 0 && any ( \ key -> key == Shift_L || key == Shift_R ) pressedKeys
|
||||||
|
= let
|
||||||
|
newCenter :: Point2D Double
|
||||||
|
newCenter = ( ( 25 / zoomFactor ) *^ Vector2D ( Point2D dy 0 ) ) • viewportCenter
|
||||||
|
in doc { viewportCenter = newCenter }
|
||||||
|
-- Vertical scrolling.
|
||||||
|
| otherwise
|
||||||
|
= let
|
||||||
|
newCenter :: Point2D Double
|
||||||
|
newCenter = ( ( 25 / zoomFactor ) *^ Vector2D ( Point2D dx dy ) ) • viewportCenter
|
||||||
|
in doc { viewportCenter = newCenter }
|
||||||
|
docs' :: IntMap Document
|
||||||
|
docs' = IntMap.insert i newDoc docs
|
||||||
|
STM.atomically ( STM.writeTVar openDocumentsTVar docs' )
|
||||||
|
GTK.widgetQueueDraw viewportArea
|
||||||
|
pure True
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
-- Rendering
|
||||||
|
|
||||||
|
void $ GTK.onWidgetDraw viewportArea \ctx -> do
|
||||||
|
-- Get the relevant document information
|
||||||
|
mbActiveDoc <- STM.readTVarIO activeDocumentTVar
|
||||||
|
for_ mbActiveDoc \ i -> do
|
||||||
|
docs <- STM.readTVarIO openDocumentsTVar
|
||||||
|
for_ ( IntMap.lookup i docs ) \ doc -> do
|
||||||
|
( `Cairo.renderWithContext` ctx ) $ do
|
||||||
|
viewportWidth <- GTK.widgetGetAllocatedWidth viewportArea
|
||||||
|
viewportHeight <- GTK.widgetGetAllocatedHeight viewportArea
|
||||||
|
renderDocument colours ( viewportWidth, viewportHeight ) doc
|
||||||
|
pure True
|
||||||
|
|
||||||
|
---------------------------------------------------------
|
||||||
|
-- File bar
|
||||||
|
|
||||||
|
widgetAddClass fileBar "fileBar"
|
||||||
|
|
||||||
|
fileTabs <- GTK.boxNew GTK.OrientationHorizontal 0
|
||||||
|
GTK.containerAdd fileBar fileTabs
|
||||||
|
widgetAddClasses fileTabs [ "fileBar", "plain", "text" ]
|
||||||
|
|
||||||
|
fileBarPhantomRadioButton <- GTK.radioButtonNew ( [] @GTK.RadioButton )
|
||||||
|
|
||||||
|
-- TODO: currently using static list of documents.
|
||||||
|
-- Need to dynamically update this widget as the user opens/closes documents.
|
||||||
|
fileButtons <- ( `IntMap.traverseWithKey` testDocuments ) \ i ( Document { displayName, unsavedChanges } ) -> do
|
||||||
|
-- File tab elements.
|
||||||
|
pgButton <- GTK.radioButtonNewWithLabelFromWidget ( Just fileBarPhantomRadioButton ) displayName
|
||||||
|
GTK.toggleButtonSetMode pgButton False -- don't display radio indicator
|
||||||
|
closeFileButton <- GTK.buttonNewWithLabel "x"
|
||||||
|
|
||||||
|
-- Create box for file tab elements.
|
||||||
|
tab <- GTK.boxNew GTK.OrientationHorizontal 0
|
||||||
|
widgetAddClasses tab [ "fileBarTab" ]
|
||||||
|
GTK.boxPackStart fileTabs tab False False 0
|
||||||
|
GTK.boxPackStart tab pgButton True True 0
|
||||||
|
GTK.boxPackStart tab closeFileButton False False 0
|
||||||
|
|
||||||
|
widgetAddClasses pgButton [ "fileBarTabButton" ]
|
||||||
|
widgetAddClasses closeFileButton [ "fileBarCloseButton" ]
|
||||||
|
|
||||||
|
-- Make both file tab elements activate styling on the whole tab
|
||||||
|
-- (e.g. hovering over the close file button should highlight the whole tab).
|
||||||
|
void $ GTK.onButtonClicked pgButton do
|
||||||
|
isActive <- GTK.toggleButtonGetActive pgButton
|
||||||
|
flags <- GTK.widgetGetStateFlags tab
|
||||||
|
if isActive
|
||||||
|
then do
|
||||||
|
GTK.widgetSetStateFlags tab ( GTK.StateFlagsActive : flags ) True
|
||||||
|
STM.atomically ( STM.writeTVar activeDocumentTVar ( Just i ) )
|
||||||
|
GTK.widgetQueueDraw viewportArea
|
||||||
|
else
|
||||||
|
GTK.widgetSetStateFlags tab ( filter ( /= GTK.StateFlagsActive ) flags ) True
|
||||||
|
|
||||||
|
{-
|
||||||
|
void $ GTK.onButtonClicked closeFileButton do
|
||||||
|
closeFileDialog ...
|
||||||
|
-}
|
||||||
|
|
||||||
|
for_ @_ @_ @_ @() [ Exists @GTK.IsWidget pgButton, Exists @GTK.IsWidget closeFileButton ] \ ( Exists button ) -> do
|
||||||
|
void $ GTK.onWidgetEnterNotifyEvent button \ _ -> do
|
||||||
|
flags <- GTK.widgetGetStateFlags tab
|
||||||
|
GTK.widgetSetStateFlags tab ( GTK.StateFlagsPrelight : flags ) True
|
||||||
|
pure False
|
||||||
|
void $ GTK.onWidgetLeaveNotifyEvent button \ _ -> do
|
||||||
|
flags <- GTK.widgetGetStateFlags tab
|
||||||
|
GTK.widgetSetStateFlags tab ( filter ( /= GTK.StateFlagsPrelight ) flags ) True
|
||||||
|
pure False
|
||||||
|
|
||||||
|
pure pgButton
|
||||||
|
|
||||||
|
GTK.scrolledWindowSetPolicy fileBar GTK.PolicyTypeAutomatic GTK.PolicyTypeNever
|
||||||
|
GTK.scrolledWindowSetOverlayScrolling fileBar True
|
||||||
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
-- Panels
|
-- Panels
|
||||||
|
|
||||||
|
@ -469,7 +612,6 @@ main = do
|
||||||
GTK.boxPackStart brushesPanel brushesContent True True 0
|
GTK.boxPackStart brushesPanel brushesContent True True 0
|
||||||
GTK.boxPackStart transformPanel transformContent True True 0
|
GTK.boxPackStart transformPanel transformContent True True 0
|
||||||
|
|
||||||
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
-- Info bar
|
-- Info bar
|
||||||
|
|
||||||
|
@ -554,7 +696,10 @@ main = do
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
-- Actions
|
-- Actions
|
||||||
|
|
||||||
GTK.widgetAddEvents window [GDK.EventMaskKeyPressMask, GDK.EventMaskKeyReleaseMask]
|
GTK.widgetAddEvents window
|
||||||
|
[ GDK.EventMaskKeyPressMask, GDK.EventMaskKeyReleaseMask ]
|
||||||
|
GTK.widgetAddEvents viewportArea
|
||||||
|
[ GDK.EventMaskScrollMask, GDK.EventMaskSmoothScrollMask ]
|
||||||
|
|
||||||
_ <- GTK.onButtonClicked closeButton GTK.mainQuit
|
_ <- GTK.onButtonClicked closeButton GTK.mainQuit
|
||||||
_ <- GTK.onButtonClicked minimiseButton ( GTK.windowIconify window )
|
_ <- GTK.onButtonClicked minimiseButton ( GTK.windowIconify window )
|
||||||
|
@ -568,8 +713,8 @@ main = do
|
||||||
then GTK.windowUnmaximize window
|
then GTK.windowUnmaximize window
|
||||||
else GTK.windowMaximize window
|
else GTK.windowMaximize window
|
||||||
|
|
||||||
_ <- GTK.onWidgetKeyPressEvent window handleKeyboardPressEvent
|
_ <- GTK.onWidgetKeyPressEvent window ( handleKeyboardPressEvent pressedKeysTVar )
|
||||||
_ <- GTK.onWidgetKeyReleaseEvent window handleKeyboardReleaseEvent
|
_ <- GTK.onWidgetKeyReleaseEvent window ( handleKeyboardReleaseEvent pressedKeysTVar )
|
||||||
_ <- GTK.onWidgetDestroy window GTK.mainQuit
|
_ <- GTK.onWidgetDestroy window GTK.mainQuit
|
||||||
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
|
@ -580,6 +725,8 @@ main = do
|
||||||
|
|
||||||
exitSuccess
|
exitSuccess
|
||||||
|
|
||||||
|
---------------------------------------------------------
|
||||||
|
-- Utils.
|
||||||
|
|
||||||
data Exists c where
|
data Exists c where
|
||||||
Exists :: c a => a -> Exists c
|
Exists :: c a => a -> Exists c
|
|
@ -2,7 +2,6 @@ packages: .
|
||||||
|
|
||||||
constraints:
|
constraints:
|
||||||
acts -finitary
|
acts -finitary
|
||||||
, haskell-gi >= 0.24
|
|
||||||
|
|
||||||
-- fixes gi-cairo-render to work with haskell-gi >= 0.24
|
-- fixes gi-cairo-render to work with haskell-gi >= 0.24
|
||||||
source-repository-package
|
source-repository-package
|
||||||
|
|
|
@ -1,14 +1,38 @@
|
||||||
|
{-# LANGUAGE DerivingStrategies #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
|
||||||
module MetaBrush.Document where
|
module MetaBrush.Document where
|
||||||
|
|
||||||
|
-- text
|
||||||
|
import Data.Text
|
||||||
|
( Text )
|
||||||
|
|
||||||
|
-- MetaBrush
|
||||||
|
import Math.Vector2D
|
||||||
|
( Point2D )
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
{-
|
data AABB
|
||||||
|
= AABB
|
||||||
|
{ topLeft :: !( Point2D Double )
|
||||||
|
, botRight :: !( Point2D Double )
|
||||||
|
}
|
||||||
|
deriving stock Show
|
||||||
|
|
||||||
data Document
|
data Document
|
||||||
= Document
|
= Document
|
||||||
{ displayName :: !Text
|
{ displayName :: !Text
|
||||||
, filePath :: !( Maybe FilePath )
|
, filePath :: !( Maybe FilePath )
|
||||||
, unsavedChanges :: !Bool
|
, unsavedChanges :: !Bool
|
||||||
, viewport :: !AABB
|
, strokes :: ![ Stroke ]
|
||||||
, strokes :: !(Set Stroke)
|
, bounds :: !AABB
|
||||||
|
, viewportCenter :: !( Point2D Double )
|
||||||
|
, zoomFactor :: !Double
|
||||||
}
|
}
|
||||||
-}
|
deriving stock Show
|
||||||
|
|
||||||
|
data Stroke
|
||||||
|
= Stroke
|
||||||
|
{ strokePoints :: ![ Point2D Double ] }
|
||||||
|
deriving stock Show
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
module MetaBrush.Event
|
{-# LANGUAGE BlockArguments #-}
|
||||||
( handleKeyboardPressEvent
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
, handleKeyboardReleaseEvent
|
|
||||||
)
|
module MetaBrush.Event where
|
||||||
where
|
|
||||||
|
-- base
|
||||||
|
import Data.Word
|
||||||
|
( Word32 )
|
||||||
|
|
||||||
-- gi-gdk
|
-- gi-gdk
|
||||||
import qualified GI.Gdk as GDK
|
import qualified GI.Gdk as GDK
|
||||||
|
@ -10,14 +13,67 @@ import qualified GI.Gdk as GDK
|
||||||
-- gi-gtk
|
-- gi-gtk
|
||||||
import qualified GI.Gtk as GTK
|
import qualified GI.Gtk as GTK
|
||||||
|
|
||||||
|
-- stm
|
||||||
|
import qualified Control.Concurrent.STM as STM
|
||||||
|
( atomically )
|
||||||
|
import qualified Control.Concurrent.STM.TVar as STM
|
||||||
|
( TVar, readTVar, writeTVar )
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
handleKeyboardPressEvent, handleKeyboardReleaseEvent :: GDK.EventKey -> IO Bool
|
handleKeyboardPressEvent, handleKeyboardReleaseEvent :: STM.TVar [ Word32 ] -> GDK.EventKey -> IO Bool
|
||||||
handleKeyboardPressEvent evt = do
|
handleKeyboardPressEvent pressedKeysTVar evt = do
|
||||||
keyCode <- GDK.getEventKeyKeyval evt
|
keyCode <- GDK.getEventKeyKeyval evt
|
||||||
case keyCode of
|
case keyCode of
|
||||||
-- escape
|
Escape -> GTK.mainQuit
|
||||||
0xff1b -> GTK.mainQuit
|
_ -> STM.atomically do
|
||||||
_ -> pure ()
|
pressedKeys <- STM.readTVar pressedKeysTVar
|
||||||
|
STM.writeTVar pressedKeysTVar ( keyCode : pressedKeys )
|
||||||
pure True
|
pure True
|
||||||
handleKeyboardReleaseEvent _ = pure True
|
handleKeyboardReleaseEvent pressedKeysTVar evt = do
|
||||||
|
keyCode <- GDK.getEventKeyKeyval evt
|
||||||
|
STM.atomically do
|
||||||
|
pressedKeys <- STM.readTVar pressedKeysTVar
|
||||||
|
STM.writeTVar pressedKeysTVar ( filter ( /= keyCode ) pressedKeys )
|
||||||
|
pure True
|
||||||
|
|
||||||
|
pattern Escape :: Word32
|
||||||
|
pattern Escape = 0xff1b
|
||||||
|
pattern Delete :: Word32
|
||||||
|
pattern Delete = 0xffff
|
||||||
|
pattern BackSpace :: Word32
|
||||||
|
pattern BackSpace = 0xff08
|
||||||
|
pattern Tab :: Word32
|
||||||
|
pattern Tab = 0xff09
|
||||||
|
pattern Return :: Word32
|
||||||
|
pattern Return = 0xff0d
|
||||||
|
pattern Pause :: Word32
|
||||||
|
pattern Pause = 0xff13
|
||||||
|
pattern Left :: Word32
|
||||||
|
pattern Left = 0xff51
|
||||||
|
pattern Up :: Word32
|
||||||
|
pattern Up = 0xff52
|
||||||
|
pattern Right :: Word32
|
||||||
|
pattern Right = 0xff53
|
||||||
|
pattern Down :: Word32
|
||||||
|
pattern Down = 0xff54
|
||||||
|
pattern PageUp :: Word32
|
||||||
|
pattern PageUp = 0xff55
|
||||||
|
pattern Next :: Word32
|
||||||
|
pattern Next = 0xff56
|
||||||
|
pattern PageDown :: Word32
|
||||||
|
pattern PageDown = 0xff56
|
||||||
|
pattern End :: Word32
|
||||||
|
pattern End = 0xff57
|
||||||
|
pattern Shift_L :: Word32
|
||||||
|
pattern Shift_L = 0xffe1
|
||||||
|
pattern Shift_R :: Word32
|
||||||
|
pattern Shift_R = 0xffe2
|
||||||
|
pattern Control_L :: Word32
|
||||||
|
pattern Control_L = 0xffe3
|
||||||
|
pattern Control_R :: Word32
|
||||||
|
pattern Control_R = 0xffe4
|
||||||
|
pattern Alt_L :: Word32
|
||||||
|
pattern Alt_L = 0xffe9
|
||||||
|
pattern Alt_R :: Word32
|
||||||
|
pattern Alt_R = 0xffea
|
||||||
|
|
125
src/app/MetaBrush/Render/Document.hs
Normal file
125
src/app/MetaBrush/Render/Document.hs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE NegativeLiterals #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
module MetaBrush.Render.Document
|
||||||
|
( renderDocument )
|
||||||
|
where
|
||||||
|
|
||||||
|
-- base
|
||||||
|
import Control.Monad
|
||||||
|
( when )
|
||||||
|
import Data.Int
|
||||||
|
( Int32 )
|
||||||
|
|
||||||
|
-- gi-cairo-render
|
||||||
|
import qualified GI.Cairo.Render as Cairo
|
||||||
|
|
||||||
|
-- MetaBrush
|
||||||
|
import qualified Math.Bezier.Quadratic as Quadratic
|
||||||
|
( Bezier(..) )
|
||||||
|
import Math.Vector2D
|
||||||
|
( Point2D(..) )
|
||||||
|
import MetaBrush.Asset.Colours
|
||||||
|
( Colours, ColourRecord(..) )
|
||||||
|
import MetaBrush.Document
|
||||||
|
( Document(..), Stroke(..) )
|
||||||
|
import MetaBrush.Render.Util
|
||||||
|
( withRGBA )
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
renderDocument :: Colours -> ( Int32, Int32 ) -> Document -> Cairo.Render ()
|
||||||
|
renderDocument cols ( viewportWidth, viewportHeight ) ( Document { viewportCenter = Point2D cx cy, .. } ) = do
|
||||||
|
|
||||||
|
Cairo.save
|
||||||
|
Cairo.translate ( 0.5 * fromIntegral viewportWidth ) ( 0.5 * fromIntegral viewportHeight )
|
||||||
|
Cairo.scale zoomFactor zoomFactor
|
||||||
|
Cairo.translate ( -cx ) ( -cy )
|
||||||
|
|
||||||
|
let
|
||||||
|
( renderPoints, renderPath ) = renderStrokes cols zoomFactor strokes
|
||||||
|
renderPath
|
||||||
|
renderPoints
|
||||||
|
|
||||||
|
Cairo.restore
|
||||||
|
|
||||||
|
pure ()
|
||||||
|
|
||||||
|
renderStrokes :: Colours -> Double -> [ Stroke ] -> ( Cairo.Render (), Cairo.Render () )
|
||||||
|
renderStrokes _ _ [] = ( pure (), pure () )
|
||||||
|
renderStrokes cols zoom ( s : ss ) = ( points1 *> points2, path1 *> path2 )
|
||||||
|
where
|
||||||
|
( points1, path1 ) = renderStroke cols zoom s
|
||||||
|
( points2, path2 ) = renderStrokes cols zoom ss
|
||||||
|
|
||||||
|
renderStroke :: Colours -> Double -> Stroke -> ( Cairo.Render (), Cairo.Render () )
|
||||||
|
renderStroke cols zoom ( Stroke strokePts ) = go True strokePts
|
||||||
|
where
|
||||||
|
go :: Bool -> [ Point2D Double ] -> ( Cairo.Render (), Cairo.Render () )
|
||||||
|
go drawFirstPoint pts = case pts of
|
||||||
|
[] -> ( pure (), pure () )
|
||||||
|
[p0] -> ( when drawFirstPoint ( drawPoint cols zoom p0 ), pure () )
|
||||||
|
[_,_] -> error "'renderStroke': unexpected pair of points"
|
||||||
|
( p0 : p1 : p2 : ps ) ->
|
||||||
|
let
|
||||||
|
drawPoints, drawNextPoints, drawPath, drawNextPath :: Cairo.Render ()
|
||||||
|
( drawNextPoints, drawNextPath ) = go False ( p2 : ps )
|
||||||
|
drawPoints = do
|
||||||
|
when drawFirstPoint ( drawPoint cols zoom p0 )
|
||||||
|
drawControl cols zoom p1
|
||||||
|
drawPoint cols zoom p2
|
||||||
|
drawNextPoints
|
||||||
|
drawPath = do
|
||||||
|
drawQuadraticBezier cols zoom ( Quadratic.Bezier p0 p1 p2 )
|
||||||
|
drawNextPath
|
||||||
|
in ( drawPoints, drawPath )
|
||||||
|
|
||||||
|
|
||||||
|
drawPoint, drawControl :: Colours -> Double -> Point2D Double -> Cairo.Render ()
|
||||||
|
drawPoint ( Colours { pathPoint, pathPointOutline } ) zoom ( Point2D x y ) = do
|
||||||
|
|
||||||
|
let
|
||||||
|
hsqrt3 :: Double
|
||||||
|
hsqrt3 = sqrt 0.75
|
||||||
|
|
||||||
|
Cairo.save
|
||||||
|
Cairo.translate x y
|
||||||
|
Cairo.scale ( 4 / zoom ) ( 4 / zoom )
|
||||||
|
|
||||||
|
Cairo.moveTo 1 0
|
||||||
|
Cairo.lineTo 0.5 hsqrt3
|
||||||
|
Cairo.lineTo -0.5 hsqrt3
|
||||||
|
Cairo.lineTo -1 0
|
||||||
|
Cairo.lineTo -0.5 (-hsqrt3)
|
||||||
|
Cairo.lineTo 0.5 (-hsqrt3)
|
||||||
|
Cairo.closePath
|
||||||
|
|
||||||
|
Cairo.setLineWidth 1
|
||||||
|
withRGBA pathPointOutline Cairo.setSourceRGBA
|
||||||
|
Cairo.strokePreserve
|
||||||
|
|
||||||
|
withRGBA pathPoint Cairo.setSourceRGBA
|
||||||
|
Cairo.fill
|
||||||
|
|
||||||
|
Cairo.restore
|
||||||
|
|
||||||
|
drawControl ( Colours { controlPoint, controlPointOutline } ) zoom ( Point2D x y ) = do
|
||||||
|
|
||||||
|
Cairo.save
|
||||||
|
Cairo.translate x y
|
||||||
|
Cairo.scale ( 4 / zoom ) ( 4 / zoom )
|
||||||
|
|
||||||
|
Cairo.arc 0 0 1 0 ( 2 * pi )
|
||||||
|
|
||||||
|
Cairo.setLineWidth 1
|
||||||
|
withRGBA controlPointOutline Cairo.setSourceRGBA
|
||||||
|
Cairo.strokePreserve
|
||||||
|
|
||||||
|
withRGBA controlPoint Cairo.setSourceRGBA
|
||||||
|
Cairo.fill
|
||||||
|
|
||||||
|
Cairo.restore
|
||||||
|
|
||||||
|
drawQuadraticBezier :: Colours -> Double -> Quadratic.Bezier ( Point2D Double ) -> Cairo.Render ()
|
||||||
|
drawQuadraticBezier _ _ _ = pure ()
|
|
@ -14,6 +14,7 @@
|
||||||
module Math.Bezier.Cubic
|
module Math.Bezier.Cubic
|
||||||
( Bezier(..)
|
( Bezier(..)
|
||||||
, bezier, bezier'
|
, bezier, bezier'
|
||||||
|
, subdivide
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
@ -49,10 +50,6 @@ data Bezier p
|
||||||
}
|
}
|
||||||
deriving stock ( Show, Generic, Functor, Foldable, Traversable )
|
deriving stock ( Show, Generic, Functor, Foldable, Traversable )
|
||||||
|
|
||||||
instance Module r p => Module r ( Bezier p ) where
|
|
||||||
( Bezier p0 p1 p2 p3 ) ^+^ ( Bezier q0 q1 q2 q3 ) = Bezier ( p0 ^+^ q0 ) ( p1 ^+^ q1 ) ( p2 ^+^ q2 ) ( p3 ^+^ q3 )
|
|
||||||
r *^ bz = fmap ( r *^ ) bz
|
|
||||||
|
|
||||||
-- | Cubic Bézier curve.
|
-- | Cubic Bézier curve.
|
||||||
bezier :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> p
|
bezier :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> p
|
||||||
bezier ( Bezier { .. } ) t =
|
bezier ( Bezier { .. } ) t =
|
||||||
|
@ -67,3 +64,15 @@ bezier' ( Bezier { .. } ) t
|
||||||
$ lerp @v t
|
$ lerp @v t
|
||||||
( lerp @v t ( p0 --> p1 ) ( p1 --> p2 ) )
|
( lerp @v t ( p0 --> p1 ) ( p1 --> p2 ) )
|
||||||
( lerp @v t ( p1 --> p2 ) ( p2 --> p3 ) )
|
( lerp @v t ( p1 --> p2 ) ( p2 --> p3 ) )
|
||||||
|
|
||||||
|
-- | Subdivide a cubic Bézier curve into two parts.
|
||||||
|
subdivide :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> ( Bezier p, Bezier p )
|
||||||
|
subdivide ( Bezier { .. } ) t = ( Bezier p0 q1 q2 pt, Bezier pt r1 r2 p3 )
|
||||||
|
where
|
||||||
|
pt, s, q1, q2, r1, r2 :: p
|
||||||
|
q1 = lerp @v t p0 p1
|
||||||
|
s = lerp @v t p1 p2
|
||||||
|
r2 = lerp @v t p2 p3
|
||||||
|
q2 = lerp @v t q1 s
|
||||||
|
r1 = lerp @v t s r2
|
||||||
|
pt = lerp @v t q2 r1
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
module Math.Bezier.Quadratic
|
module Math.Bezier.Quadratic
|
||||||
( Bezier(..)
|
( Bezier(..)
|
||||||
, bezier, bezier'
|
, bezier, bezier'
|
||||||
|
, subdivide
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
@ -46,10 +47,6 @@ data Bezier p
|
||||||
}
|
}
|
||||||
deriving stock ( Show, Generic, Functor, Foldable, Traversable )
|
deriving stock ( Show, Generic, Functor, Foldable, Traversable )
|
||||||
|
|
||||||
instance Module r p => Module r ( Bezier p ) where
|
|
||||||
( Bezier p0 p1 p2 ) ^+^ ( Bezier q0 q1 q2 ) = Bezier ( p0 ^+^ q0 ) ( p1 ^+^ q1 ) ( p2 ^+^ q2 )
|
|
||||||
r *^ bz = fmap ( r *^ ) bz
|
|
||||||
|
|
||||||
-- | Quadratic Bézier curve.
|
-- | Quadratic Bézier curve.
|
||||||
bezier :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> p
|
bezier :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> p
|
||||||
bezier ( Bezier { .. } ) t = lerp @v t ( lerp @v t p0 p1 ) ( lerp @v t p1 p2 )
|
bezier ( Bezier { .. } ) t = lerp @v t ( lerp @v t p0 p1 ) ( lerp @v t p1 p2 )
|
||||||
|
@ -57,3 +54,12 @@ bezier ( Bezier { .. } ) t = lerp @v t ( lerp @v t p0 p1 ) ( lerp @v t p1 p2 )
|
||||||
-- | Derivative of quadratic Bézier curve.
|
-- | Derivative of quadratic Bézier curve.
|
||||||
bezier' :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> v
|
bezier' :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> v
|
||||||
bezier' ( Bezier { .. } ) t = 2 *^ lerp @v t ( p0 --> p1 ) ( p1 --> p2 )
|
bezier' ( Bezier { .. } ) t = 2 *^ lerp @v t ( p0 --> p1 ) ( p1 --> p2 )
|
||||||
|
|
||||||
|
-- | Subdivide a quadratic Bézier curve into two parts.
|
||||||
|
subdivide :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> ( Bezier p, Bezier p )
|
||||||
|
subdivide ( Bezier { .. } ) t = ( Bezier p0 q1 pt, Bezier pt r1 p2 )
|
||||||
|
where
|
||||||
|
pt, q1, r1 :: p
|
||||||
|
q1 = lerp @v t p0 p1
|
||||||
|
r1 = lerp @v t p1 p2
|
||||||
|
pt = lerp @v t q1 r1
|
||||||
|
|
Loading…
Reference in a new issue