2024-09-27 15:21:24 +00:00
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
|
|
|
|
|
|
module MetaBrush.Stroke where
|
|
|
|
|
|
|
|
|
|
-- base
|
2024-09-27 21:36:33 +00:00
|
|
|
|
import Control.Arrow
|
|
|
|
|
( (***) )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
import Control.Monad.ST
|
2024-10-09 14:57:08 +00:00
|
|
|
|
( ST, RealWorld, runST )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
import Data.Coerce
|
|
|
|
|
( coerce )
|
|
|
|
|
import Data.Foldable
|
|
|
|
|
( foldr' )
|
|
|
|
|
import Data.Functor.Identity
|
|
|
|
|
( Identity(..) )
|
|
|
|
|
import Data.Typeable
|
|
|
|
|
( Typeable )
|
|
|
|
|
import GHC.Generics
|
|
|
|
|
( Generic, Generic1 )
|
|
|
|
|
import GHC.Stack
|
|
|
|
|
( HasCallStack )
|
|
|
|
|
import GHC.TypeLits
|
|
|
|
|
( Symbol )
|
2024-10-09 14:57:08 +00:00
|
|
|
|
import Unsafe.Coerce
|
|
|
|
|
( unsafeCoerce )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
|
|
|
|
|
-- acts
|
|
|
|
|
import Data.Act
|
|
|
|
|
( Act(..), Torsor(..) )
|
|
|
|
|
|
|
|
|
|
-- containers
|
|
|
|
|
import qualified Data.Map.Strict as Map
|
|
|
|
|
import qualified Data.Set as Set
|
|
|
|
|
|
|
|
|
|
-- deepseq
|
|
|
|
|
import Control.DeepSeq
|
|
|
|
|
( NFData(..), NFData1 )
|
|
|
|
|
|
|
|
|
|
-- generic-lens
|
|
|
|
|
import Data.Generics.Product.Fields
|
|
|
|
|
( field' )
|
|
|
|
|
|
|
|
|
|
-- groups
|
|
|
|
|
import Data.Group
|
|
|
|
|
( Group(..) )
|
|
|
|
|
|
|
|
|
|
-- lens
|
|
|
|
|
import Control.Lens
|
|
|
|
|
( Lens'
|
|
|
|
|
, view, over
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
-- text
|
|
|
|
|
import Data.Text
|
|
|
|
|
( Text )
|
|
|
|
|
|
|
|
|
|
-- transformers
|
2024-09-27 21:36:33 +00:00
|
|
|
|
import Control.Monad.IO.Class
|
|
|
|
|
( MonadIO )
|
|
|
|
|
import Control.Monad.Trans.Reader
|
|
|
|
|
( ReaderT )
|
|
|
|
|
import Control.Monad.Trans.State.Strict
|
|
|
|
|
( StateT )
|
|
|
|
|
import qualified Control.Monad.Trans.State.Strict as State
|
2024-09-27 15:21:24 +00:00
|
|
|
|
|
|
|
|
|
-- brush-strokes
|
|
|
|
|
import Math.Bezier.Spline
|
|
|
|
|
( Spline(..), KnownSplineType
|
2024-10-09 14:57:08 +00:00
|
|
|
|
, PointType(..), bitraverseSpline, bitraverseCurve
|
2024-09-27 15:21:24 +00:00
|
|
|
|
)
|
|
|
|
|
import Math.Bezier.Stroke
|
2024-10-09 14:57:08 +00:00
|
|
|
|
( CachedStroke, newCache )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
import Math.Module
|
|
|
|
|
( Module
|
|
|
|
|
( origin, (^+^), (^-^), (*^) )
|
|
|
|
|
)
|
|
|
|
|
import Math.Linear
|
|
|
|
|
( ℝ(..), T(..) )
|
|
|
|
|
|
|
|
|
|
-- MetaBrush
|
|
|
|
|
import MetaBrush.Brush
|
|
|
|
|
( NamedBrush, PointFields )
|
2024-09-27 21:36:33 +00:00
|
|
|
|
import MetaBrush.Layer
|
|
|
|
|
( Hierarchy(..), LayerMetadata(..), emptyHierarchy )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
import MetaBrush.Records
|
|
|
|
|
import MetaBrush.Unique
|
2024-09-27 21:36:33 +00:00
|
|
|
|
( Unique, UniqueSupply, freshUnique )
|
|
|
|
|
import MetaBrush.Util
|
|
|
|
|
( (!) )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
-- | Data attached to each point on a spline.
|
|
|
|
|
data PointData params
|
|
|
|
|
= PointData
|
|
|
|
|
{ pointCoords :: !( ℝ 2 )
|
|
|
|
|
, brushParams :: !params
|
|
|
|
|
}
|
|
|
|
|
deriving stock ( Show, Generic )
|
|
|
|
|
deriving anyclass NFData
|
|
|
|
|
|
|
|
|
|
-- | Data attached to each curve in a spline.
|
2024-09-28 11:07:56 +00:00
|
|
|
|
data CurveData s =
|
2024-09-27 15:21:24 +00:00
|
|
|
|
CurveData
|
|
|
|
|
{ curveIndex :: !Rational
|
2024-09-28 11:07:56 +00:00
|
|
|
|
, cachedStroke :: !( CachedStroke s )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
}
|
|
|
|
|
deriving stock Generic
|
|
|
|
|
deriving anyclass NFData
|
|
|
|
|
|
2024-09-28 11:07:56 +00:00
|
|
|
|
instance Show ( CurveData s ) where
|
2024-09-27 15:21:24 +00:00
|
|
|
|
show ( CurveData { curveIndex } ) = show curveIndex
|
2024-09-28 11:07:56 +00:00
|
|
|
|
instance Eq ( CurveData s ) where
|
2024-09-27 15:21:24 +00:00
|
|
|
|
( CurveData { curveIndex = i1 } ) == ( CurveData { curveIndex = i2 } )
|
|
|
|
|
= i1 == i2
|
2024-09-28 11:07:56 +00:00
|
|
|
|
instance Ord ( CurveData s ) where
|
2024-09-27 15:21:24 +00:00
|
|
|
|
compare ( CurveData { curveIndex = i1 } ) ( CurveData { curveIndex = i2 } )
|
|
|
|
|
= compare i1 i2
|
|
|
|
|
|
|
|
|
|
-- | An index for a point on a spline.
|
|
|
|
|
data PointIndex
|
|
|
|
|
= FirstPoint
|
|
|
|
|
| PointIndex
|
|
|
|
|
-- | Which curve the point belongs to.
|
|
|
|
|
{ pointCurve :: !Rational
|
|
|
|
|
-- | Index within a curve.
|
|
|
|
|
, pointType :: !PointType
|
|
|
|
|
}
|
|
|
|
|
deriving stock ( Show, Eq, Ord, Generic )
|
|
|
|
|
deriving anyclass NFData
|
|
|
|
|
|
|
|
|
|
_coords :: Lens' ( PointData brushParams ) ( ℝ 2 )
|
|
|
|
|
_coords = field' @"pointCoords"
|
|
|
|
|
|
|
|
|
|
coords :: PointData brushParams -> ℝ 2
|
|
|
|
|
coords = view _coords
|
|
|
|
|
|
|
|
|
|
type StrokeSpline clo brushParams =
|
2024-09-28 11:07:56 +00:00
|
|
|
|
Spline clo ( CurveData RealWorld ) ( PointData brushParams )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
|
|
|
|
|
data Stroke where
|
|
|
|
|
Stroke
|
|
|
|
|
:: forall clo pointParams ( pointFields :: [ Symbol ] ) ( brushFields :: [ Symbol ] )
|
|
|
|
|
. ( KnownSplineType clo
|
|
|
|
|
, pointParams ~ Record pointFields
|
|
|
|
|
, PointFields pointFields, Typeable pointFields
|
|
|
|
|
)
|
|
|
|
|
=>
|
|
|
|
|
{ strokeBrush :: !( Maybe ( NamedBrush brushFields ) )
|
|
|
|
|
, strokeSpline :: !( StrokeSpline clo pointParams )
|
|
|
|
|
}
|
|
|
|
|
-> Stroke
|
|
|
|
|
deriving stock instance Show Stroke
|
|
|
|
|
instance NFData Stroke where
|
|
|
|
|
rnf ( Stroke { strokeBrush, strokeSpline } )
|
|
|
|
|
= rnf strokeBrush `seq` rnf strokeSpline
|
|
|
|
|
|
|
|
|
|
_strokeSpline
|
|
|
|
|
:: forall f
|
|
|
|
|
. Functor f
|
|
|
|
|
=> ( forall clo pointParams ( pointFields :: [ Symbol ] )
|
|
|
|
|
. ( KnownSplineType clo
|
|
|
|
|
, pointParams ~ Record pointFields
|
|
|
|
|
, PointFields pointFields
|
|
|
|
|
)
|
|
|
|
|
=> StrokeSpline clo pointParams
|
|
|
|
|
-> f ( StrokeSpline clo pointParams )
|
|
|
|
|
)
|
|
|
|
|
-> Stroke -> f Stroke
|
|
|
|
|
_strokeSpline f ( Stroke { strokeSpline = oldStrokeSpline, .. } )
|
|
|
|
|
= ( \ newSpline -> Stroke { strokeSpline = newSpline, .. } ) <$> f oldStrokeSpline
|
|
|
|
|
|
|
|
|
|
overStrokeSpline
|
|
|
|
|
:: ( forall clo pointParams ( pointFields :: [ Symbol ] )
|
|
|
|
|
. ( KnownSplineType clo
|
|
|
|
|
, pointParams ~ Record pointFields
|
|
|
|
|
, PointFields pointFields
|
|
|
|
|
)
|
|
|
|
|
=> StrokeSpline clo pointParams
|
|
|
|
|
-> StrokeSpline clo pointParams
|
|
|
|
|
)
|
|
|
|
|
-> Stroke -> Stroke
|
|
|
|
|
overStrokeSpline f = coerce ( _strokeSpline @Identity ( coerce . f ) )
|
|
|
|
|
|
2024-10-09 14:57:08 +00:00
|
|
|
|
setStrokeBrush
|
|
|
|
|
:: ( Maybe ( NamedBrush brushFields ) )
|
|
|
|
|
-> Stroke -> Stroke
|
|
|
|
|
setStrokeBrush brush
|
|
|
|
|
( Stroke { strokeSpline = ( oldStrokeSpline :: StrokeSpline clo pointParams ) } ) =
|
|
|
|
|
-- Invalidate all of the cached brush strokes.
|
|
|
|
|
let spline' :: ST s ( Spline clo ( CurveData s ) ( PointData pointParams ) )
|
|
|
|
|
spline' = bitraverseSpline
|
|
|
|
|
( \ _ -> bitraverseCurve invalidateCurve ( const return ) )
|
|
|
|
|
return
|
|
|
|
|
oldStrokeSpline
|
|
|
|
|
in
|
|
|
|
|
Stroke
|
|
|
|
|
{ strokeSpline = runST $ coCache <$> spline'
|
|
|
|
|
, strokeBrush = brush }
|
|
|
|
|
where
|
|
|
|
|
invalidateCurve :: CurveData RealWorld -> ST s ( CurveData s )
|
|
|
|
|
invalidateCurve crv = do
|
|
|
|
|
noCache <- newCache
|
|
|
|
|
return $ crv { cachedStroke = noCache }
|
|
|
|
|
|
|
|
|
|
{-# NOINLINE coCache #-}
|
|
|
|
|
coCache :: forall s t clo ptData. Spline clo ( CurveData s ) ptData -> Spline clo ( CurveData t ) ptData
|
|
|
|
|
coCache = unsafeCoerce
|
|
|
|
|
|
2024-09-27 15:21:24 +00:00
|
|
|
|
instance Act ( T ( ℝ 2 ) ) ( PointData params ) where
|
|
|
|
|
v • ( dat@( PointData { pointCoords = p } ) ) =
|
|
|
|
|
dat { pointCoords = v • p }
|
|
|
|
|
|
|
|
|
|
data DiffPointData diffBrushParams
|
|
|
|
|
= DiffPointData
|
|
|
|
|
{ diffVector :: !( T ( ℝ 2 ) )
|
|
|
|
|
, diffParams :: !diffBrushParams
|
|
|
|
|
}
|
|
|
|
|
deriving stock ( Show, Generic, Generic1, Functor, Foldable, Traversable )
|
|
|
|
|
deriving anyclass ( NFData, NFData1 )
|
|
|
|
|
|
|
|
|
|
instance Module Double diffBrushParams => Semigroup ( DiffPointData diffBrushParams ) where
|
|
|
|
|
DiffPointData v1 p1 <> DiffPointData v2 p2 =
|
|
|
|
|
DiffPointData ( v1 <> v2 ) ( p1 ^+^ p2 )
|
|
|
|
|
instance Module Double diffBrushParams => Monoid ( DiffPointData diffBrushParams ) where
|
|
|
|
|
mempty = DiffPointData mempty origin
|
|
|
|
|
instance Module Double diffBrushParams => Group ( DiffPointData diffBrushParams ) where
|
|
|
|
|
invert ( DiffPointData v1 p1 ) =
|
|
|
|
|
DiffPointData ( invert v1 ) ( -1 *^ p1 )
|
|
|
|
|
|
|
|
|
|
instance ( Module Double diffBrushParams, Act diffBrushParams brushParams )
|
|
|
|
|
=> Act ( DiffPointData diffBrushParams ) ( PointData brushParams ) where
|
|
|
|
|
(•) ( DiffPointData { diffVector = dp, diffParams = db } )
|
|
|
|
|
= over _coords ( dp • )
|
|
|
|
|
. over ( field' @"brushParams" ) ( db • )
|
|
|
|
|
instance ( Module Double diffBrushParams, Torsor diffBrushParams brushParams )
|
|
|
|
|
=> Torsor ( DiffPointData diffBrushParams ) ( PointData brushParams ) where
|
|
|
|
|
( PointData
|
|
|
|
|
{ pointCoords = p1
|
|
|
|
|
, brushParams = b1
|
|
|
|
|
} ) <--
|
|
|
|
|
( PointData
|
|
|
|
|
{ pointCoords = p2
|
|
|
|
|
, brushParams = b2 } ) =
|
|
|
|
|
DiffPointData
|
|
|
|
|
{ diffVector = p1 <-- p2
|
|
|
|
|
, diffParams = b1 <-- b2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
instance Module Double brushParams => Module Double ( DiffPointData brushParams ) where
|
|
|
|
|
origin = mempty
|
|
|
|
|
(^+^) = (<>)
|
|
|
|
|
x ^-^ y = x <> invert y
|
|
|
|
|
d *^ DiffPointData v1 p1 =
|
|
|
|
|
DiffPointData ( d *^ v1 ) ( d *^ p1 )
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
-- | Metadata about a stroke, such as its name or its visibility.
|
|
|
|
|
data StrokeMetadata
|
|
|
|
|
= StrokeMetadata
|
|
|
|
|
{ strokeName :: !Text
|
|
|
|
|
, strokeVisible :: !Bool
|
|
|
|
|
, strokeLocked :: !Bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type StrokeHierarchy = Hierarchy Stroke
|
|
|
|
|
|
|
|
|
|
data UpdateStroke
|
|
|
|
|
= PreserveStroke
|
|
|
|
|
| DeleteStroke
|
|
|
|
|
| UpdateStrokeTo !Stroke
|
|
|
|
|
deriving stock Show
|
|
|
|
|
|
|
|
|
|
-- | Traverse through a stroke hierarchy.
|
|
|
|
|
forStrokeHierarchy
|
|
|
|
|
:: forall f
|
|
|
|
|
. ( HasCallStack, Applicative f )
|
|
|
|
|
=> LayerMetadata
|
|
|
|
|
-> StrokeHierarchy
|
|
|
|
|
-> ( Unique -> Stroke -> StrokeMetadata -> f UpdateStroke )
|
|
|
|
|
-> f StrokeHierarchy
|
|
|
|
|
forStrokeHierarchy
|
|
|
|
|
( LayerMetadata { layerNames, invisibleLayers, lockedLayers } ) hierarchy0 f =
|
|
|
|
|
foldr' ( g Nothing ( True, False ) ) ( pure hierarchy0 ) ( topLevel hierarchy0 )
|
|
|
|
|
where
|
|
|
|
|
|
|
|
|
|
insertMaybe :: Maybe Unique -> Unique -> StrokeHierarchy -> UpdateStroke -> StrokeHierarchy
|
|
|
|
|
insertMaybe mbPar u old@( Hierarchy oldTl oldGps oldStrokes ) = \case
|
|
|
|
|
PreserveStroke -> old
|
|
|
|
|
UpdateStrokeTo s -> Hierarchy oldTl oldGps ( Map.insert u s oldStrokes )
|
|
|
|
|
DeleteStroke ->
|
|
|
|
|
let newStrokes = Map.delete u oldStrokes
|
|
|
|
|
in case mbPar of
|
|
|
|
|
Nothing ->
|
|
|
|
|
Hierarchy ( filter ( /= u ) oldTl ) oldGps newStrokes
|
|
|
|
|
Just par ->
|
|
|
|
|
Hierarchy oldTl ( Map.adjust ( filter ( /= u ) ) par oldGps ) newStrokes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
g :: Maybe Unique -> ( Bool, Bool ) -> Unique -> f StrokeHierarchy -> f StrokeHierarchy
|
|
|
|
|
g par ( vis, lock ) u acc =
|
|
|
|
|
let vis' = vis && not ( u `Set.member` invisibleLayers )
|
|
|
|
|
lock' = lock || u `Set.member` lockedLayers
|
|
|
|
|
in
|
|
|
|
|
case Map.lookup u ( groups hierarchy0 ) of
|
|
|
|
|
Nothing ->
|
|
|
|
|
let
|
|
|
|
|
meta =
|
|
|
|
|
StrokeMetadata
|
2024-09-27 21:36:33 +00:00
|
|
|
|
{ strokeName = layerNames ! u
|
2024-09-27 15:21:24 +00:00
|
|
|
|
, strokeVisible = vis'
|
|
|
|
|
, strokeLocked = lock'
|
|
|
|
|
}
|
|
|
|
|
in
|
2024-09-27 21:36:33 +00:00
|
|
|
|
insertMaybe par u <$> acc <*> f u ( content hierarchy0 ! u ) meta
|
2024-09-27 15:21:24 +00:00
|
|
|
|
Just ds ->
|
|
|
|
|
foldr' ( g ( Just u ) ( vis', lock' ) ) acc ds
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
-- | Recursive representation of a stroke hierarchy.
|
|
|
|
|
--
|
|
|
|
|
-- Used for serialisation/deserialisation only.
|
|
|
|
|
type Layers = [ Layer ]
|
|
|
|
|
|
|
|
|
|
-- | Layer in a recursive representation of a stroke hierarchy.
|
|
|
|
|
--
|
|
|
|
|
-- Used for serialisation/deserialisation only.
|
|
|
|
|
data Layer
|
|
|
|
|
= StrokeLayer
|
2024-09-27 21:36:33 +00:00
|
|
|
|
{ layerName :: !Text
|
2024-09-27 15:21:24 +00:00
|
|
|
|
, layerVisible :: !Bool
|
|
|
|
|
, layerLocked :: !Bool
|
|
|
|
|
, layerStroke :: !Stroke
|
|
|
|
|
}
|
|
|
|
|
| GroupLayer
|
2024-09-27 21:36:33 +00:00
|
|
|
|
{ layerName :: !Text
|
2024-09-27 15:21:24 +00:00
|
|
|
|
, layerVisible :: !Bool
|
|
|
|
|
, layerLocked :: !Bool
|
|
|
|
|
, groupChildren :: !Layers
|
|
|
|
|
}
|
|
|
|
|
deriving stock Show
|
|
|
|
|
|
|
|
|
|
strokeHierarchyLayers :: LayerMetadata -> StrokeHierarchy -> Layers
|
|
|
|
|
strokeHierarchyLayers
|
|
|
|
|
( LayerMetadata { layerNames, invisibleLayers, lockedLayers } )
|
|
|
|
|
( Hierarchy topLevel hierarchy content ) = map go topLevel
|
|
|
|
|
where
|
|
|
|
|
go :: Unique -> Layer
|
|
|
|
|
go layerUnique =
|
|
|
|
|
let
|
2024-09-27 21:36:33 +00:00
|
|
|
|
layerName = layerNames ! layerUnique
|
2024-09-27 15:21:24 +00:00
|
|
|
|
layerVisible = not $ layerUnique `Set.member` invisibleLayers
|
|
|
|
|
layerLocked = layerUnique `Set.member` lockedLayers
|
|
|
|
|
in
|
|
|
|
|
case Map.lookup layerUnique hierarchy of
|
|
|
|
|
Nothing ->
|
|
|
|
|
StrokeLayer
|
2024-09-27 21:36:33 +00:00
|
|
|
|
{ layerName, layerVisible, layerLocked
|
|
|
|
|
, layerStroke = content ! layerUnique
|
2024-09-27 15:21:24 +00:00
|
|
|
|
}
|
|
|
|
|
Just cs ->
|
|
|
|
|
GroupLayer
|
2024-09-27 21:36:33 +00:00
|
|
|
|
{ layerName, layerVisible, layerLocked
|
2024-09-27 15:21:24 +00:00
|
|
|
|
, groupChildren = map go cs
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-27 21:36:33 +00:00
|
|
|
|
{-# INLINEABLE layersStrokeHierarchy #-}
|
|
|
|
|
layersStrokeHierarchy :: forall m. MonadIO m => Layers -> ReaderT UniqueSupply m ( LayerMetadata, StrokeHierarchy )
|
|
|
|
|
layersStrokeHierarchy lays = ( `State.execStateT` ( mempty, emptyHierarchy ) ) $ do
|
2024-09-27 15:21:24 +00:00
|
|
|
|
us <- traverse go lays
|
|
|
|
|
State.modify' ( \ ( meta, hierarchy ) -> ( meta, hierarchy { topLevel = us } ) )
|
|
|
|
|
where
|
2024-09-27 21:36:33 +00:00
|
|
|
|
go :: Layer -> StateT ( LayerMetadata, StrokeHierarchy ) ( ReaderT UniqueSupply m ) Unique
|
2024-09-27 15:21:24 +00:00
|
|
|
|
go l = do
|
2024-09-27 21:36:33 +00:00
|
|
|
|
u <- freshUnique
|
|
|
|
|
let updMeta ( LayerMetadata nms invis locked ) =
|
2024-09-27 15:21:24 +00:00
|
|
|
|
LayerMetadata
|
|
|
|
|
{ layerNames = Map.insert u ( layerName l ) nms
|
|
|
|
|
, invisibleLayers = if layerVisible l then invis else Set.insert u invis
|
|
|
|
|
, lockedLayers = if layerLocked l then Set.insert u locked else locked
|
|
|
|
|
}
|
2024-09-27 21:36:33 +00:00
|
|
|
|
updHierarchy <- case l of
|
|
|
|
|
StrokeLayer { layerStroke } ->
|
|
|
|
|
return $ \ h -> h { content = Map.insert u layerStroke ( content h ) }
|
|
|
|
|
GroupLayer { groupChildren } -> do
|
|
|
|
|
us <- traverse go groupChildren
|
|
|
|
|
return $ \ h -> h { groups = Map.insert u us ( groups h ) }
|
|
|
|
|
State.modify' ( updMeta *** updHierarchy )
|
2024-09-27 15:21:24 +00:00
|
|
|
|
return u
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|