1
Fork 0

Win a fight with svg transforms

This commit is contained in:
prescientmoon 2024-04-17 20:34:01 +02:00
parent 44ed1900d9
commit 929c8cedfb
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
12 changed files with 246 additions and 55 deletions

View file

@ -562,7 +562,7 @@ workspace:
extra_packages: extra_packages:
debugged: debugged:
git: https://github.com/mateiadrielrafael/purescript-debugged.git git: https://github.com/mateiadrielrafael/purescript-debugged.git
ref: 0d5a4149279129f10c8fe2a3ef280b9fde4d5116 ref: 70d4b2e19c8831753a62a4527b0c8c40a25b2a4e
packages: packages:
aff: aff:
type: registry type: registry
@ -694,7 +694,7 @@ packages:
debugged: debugged:
type: git type: git
url: https://github.com/mateiadrielrafael/purescript-debugged.git url: https://github.com/mateiadrielrafael/purescript-debugged.git
rev: 0d5a4149279129f10c8fe2a3ef280b9fde4d5116 rev: 70d4b2e19c8831753a62a4527b0c8c40a25b2a4e
dependencies: dependencies:
- arrays - arrays
- bifunctors - bifunctors

View file

@ -23,4 +23,5 @@ workspace:
extra_packages: extra_packages:
debugged: debugged:
git: https://github.com/mateiadrielrafael/purescript-debugged.git git: https://github.com/mateiadrielrafael/purescript-debugged.git
ref: 0d5a4149279129f10c8fe2a3ef280b9fde4d5116 ref: 70d4b2e19c8831753a62a4527b0c8c40a25b2a4e

View file

@ -36,7 +36,7 @@ type PhysicalExecutionStep = { block :: Array PhysicalKey, keys :: Array Physica
transformKey :: ScalePreservingTransform -> PhysicalKey -> PhysicalKey transformKey :: ScalePreservingTransform -> PhysicalKey -> PhysicalKey
transformKey transform (PhysicalKey key) = PhysicalKey transformKey transform (PhysicalKey key) = PhysicalKey
{ size: key.size { size: key.size
, transform: composeScalePreservingTransforms key.transform transform , transform: composeScalePreservingTransforms transform key.transform
} }
buildPhysical :: RawPhysical -> PhysicalLayout buildPhysical :: RawPhysical -> PhysicalLayout

View file

@ -3,19 +3,21 @@ module LayoutLens.Data.Geometry where
import LayoutLens.Prelude import LayoutLens.Prelude
import Data.Array as Array import Data.Array as Array
import LayoutLens.Data.Vec2 as V
import LayoutLens.Data.Vec2 import LayoutLens.Data.Vec2
( AABB(..) ( AABB(..)
, Polygon(..) , Polygon(..)
, ScalePreservingTransform
, Vec2(..) , Vec2(..)
, aabbToPolygon , aabbToPolygon
, applyScalePreservingTransform , applyTransform
, boundingBox
, mapPoints , mapPoints
, tTranslate
, vinv , vinv
, vscale , vscale
) )
data Attribute = Fill Color | Stroke Color data Attribute = Fill Color | Stroke Color | StrokeWidth Number
type Attributes = Array Attribute type Attributes = Array Attribute
type GenericAttributes = Array (String /\ String) type GenericAttributes = Array (String /\ String)
@ -31,28 +33,30 @@ data PathStep
| Close | Close
data Geometry data Geometry
= Transform ScalePreservingTransform Geometry = Transform V.Transform Geometry
| Many (Array Geometry) | Many (Array Geometry)
| Text GenericAttributes Attributes String | Text GenericAttributes Attributes String
| Rect AABB Attributes | Rect AABB Attributes
| Path (Array PathStep) Attributes | Path (Array PathStep) Attributes
| Invisible Geometry
-- Approximate the size of some geometry by fitting a polygon around it -- Approximate the size of some geometry by fitting a polygon around it
boundingPolygon :: Geometry -> Polygon boundingPolygon :: Geometry -> Maybe Polygon
boundingPolygon = case _ of boundingPolygon = case _ of
Rect aabb _ -> aabbToPolygon aabb Rect aabb _ -> pure $ aabbToPolygon aabb
Text _ _ _ -> mempty Text _ _ _ -> Nothing
Many array -> foldMap boundingPolygon array Many array -> foldMap boundingPolygon array
Transform t g -> mapPoints (applyScalePreservingTransform t) $ boundingPolygon g Invisible g -> boundingPolygon g
Transform t g -> mapPoints (applyTransform t) <$> boundingPolygon g
Path steps _ -> foldMap snd $ Array.scanl (points <<< fst) mempty steps Path steps _ -> foldMap snd $ Array.scanl (points <<< fst) mempty steps
where where
points :: Vec2 -> PathStep -> Vec2 /\ Polygon points :: Vec2 -> PathStep -> Vec2 /\ Maybe Polygon
points prev = case _ of points prev = case _ of
Close -> prev /\ Polygon [] Close -> prev /\ Nothing
MoveTo a -> a /\ Polygon [ a ] MoveTo a -> a /\ (pure $ Polygon $ pure a)
LineTo a -> a /\ Polygon [ a ] LineTo a -> a /\ (pure $ Polygon $ pure a)
-- This is just an approximation where we fit an AABB around the circle. -- This is just an approximation where we fit an AABB around the circle.
Arc arc -> arc.to /\ aabbToPolygon aabb Arc arc -> arc.to /\ pure (aabbToPolygon aabb)
where where
aabb = AABB aabb = AABB
{ position: center <> vinv diagonal { position: center <> vinv diagonal
@ -62,6 +66,21 @@ boundingPolygon = case _ of
diagonal = Vec2 arc.radius arc.radius diagonal = Vec2 arc.radius arc.radius
center = vscale 0.5 $ arc.to <> prev center = vscale 0.5 $ arc.to <> prev
-- | Add padding around some geometry
pad :: Vec2 -> Geometry -> Geometry
pad padding geometry = case boundingBox <$> boundingPolygon geometry of
Just (AABB box) -> Many
[ Transform (tTranslate padding) geometry
, Invisible $ Rect
( AABB
{ position: box.position
, size: box.size <> vscale 2.0 padding
}
)
[]
]
Nothing -> geometry
derive instance Eq Attribute derive instance Eq Attribute
derive instance Eq PathStep derive instance Eq PathStep
derive instance Eq Geometry derive instance Eq Geometry

View file

@ -4,20 +4,24 @@ import LayoutLens.Prelude
import Data.Array as Array import Data.Array as Array
import Data.String (Pattern(..)) import Data.String (Pattern(..))
import LayoutLens.Data.Geometry (Geometry(..), Attribute(..), Attributes, GenericAttributes, PathStep(..)) import LayoutLens.Data.Geometry (Attribute(..), Attributes, GenericAttributes, Geometry(..), PathStep(..), boundingPolygon)
import LayoutLens.Data.Vec2 (AABB(..), ScalePreservingTransform(..), Vec2(..), radiansToDegrees, x, y) import LayoutLens.Data.Vec2 (AABB(..), Vec2(..), boundingBox, x, y)
import LayoutLens.Data.Vec2 as V
indent :: String -> String indent :: String -> String
indent = split (Pattern "\n") >>> map (" " <> _) >>> unlines indent = split (Pattern "\n") >>> map (" " <> _) >>> unlines
printAttributes :: GenericAttributes -> String
printAttributes attributes = joinWith " "
$ uncurry (\key value -> fold [ key, "=\"", value, "\"" ])
<$> attributes
tag :: String -> GenericAttributes -> String -> String tag :: String -> GenericAttributes -> String -> String
tag name attributes child = fold tag name attributes child = fold
[ "<" [ "<"
, name , name
, " " , " "
, joinWith " " , printAttributes attributes
$ uncurry (\key value -> fold [ key, "=\"", value, "\"" ])
<$> attributes
, ">" , ">"
, indent child , indent child
, "</" , "</"
@ -26,12 +30,19 @@ tag name attributes child = fold
] ]
leaf :: String -> GenericAttributes -> String leaf :: String -> GenericAttributes -> String
leaf name attributes = tag name attributes mempty leaf name attributes = fold
[ "<"
, name
, " "
, printAttributes attributes
, "/>"
]
printGeometryAttributes :: Attributes -> GenericAttributes printGeometryAttributes :: Attributes -> GenericAttributes
printGeometryAttributes = map case _ of printGeometryAttributes = map case _ of
Fill color -> "fill" /\ toHexString color Fill color -> "fill" /\ toHexString color
Stroke color -> "stroke" /\ toHexString color Stroke color -> "stroke" /\ toHexString color
StrokeWidth number -> "stroke-width" /\ show number
px :: Number -> String px :: Number -> String
px n = show n <> "px" px n = show n <> "px"
@ -39,6 +50,7 @@ px n = show n <> "px"
-- Render a geometry to svg -- Render a geometry to svg
renderGeometry :: Geometry -> String renderGeometry :: Geometry -> String
renderGeometry = case _ of renderGeometry = case _ of
Invisible _ -> ""
Many array -> unlines $ renderGeometry <$> array Many array -> unlines $ renderGeometry <$> array
Rect (AABB aabb) proper -> Rect (AABB aabb) proper ->
@ -47,8 +59,8 @@ renderGeometry = case _ of
<> <>
[ "x" /\ show (x aabb.position) [ "x" /\ show (x aabb.position)
, "y" /\ show (y aabb.position) , "y" /\ show (y aabb.position)
, "width" /\ px (x aabb.size) , "width" /\ show (x aabb.size)
, "height" /\ px (y aabb.size) , "height" /\ show (y aabb.size)
] ]
Path steps proper -> Path steps proper ->
@ -78,12 +90,37 @@ renderGeometry = case _ of
(generic <> printGeometryAttributes proper) (generic <> printGeometryAttributes proper)
string string
Transform (ScalePreservingTransform transform) g -> Transform (V.Transform transform) g ->
tag "g" tag "g"
[ "transform" /\ fold [ "transform" /\ joinWith " "
[ "rotate(" [ "matrix("
, show $ radiansToDegrees transform.rotation , show $ transform.scale * cos (unwrap transform.rotation)
, show $ transform.scale * sin (unwrap transform.rotation)
, show $ transform.scale * -sin (unwrap transform.rotation)
, show $ transform.scale * cos (unwrap transform.rotation)
, show $ x transform.position
, show $ y transform.position
, ")" , ")"
] ]
] ]
$ renderGeometry g $ renderGeometry g
-- | Adds the necessary boilerplate to store svg inside a file
makeSvgDocument :: Geometry -> String
makeSvgDocument geometry = tag "svg" attributes $ renderGeometry geometry
where
attributes =
[ "xmlns" /\ "http://www.w3.org/2000/svg"
, "xmlns:xlink" /\ "http://www.w3.org/1999/xlink"
]
<>
case boundingBox <$> boundingPolygon geometry of
Nothing -> []
Just (AABB box) -> pure
$ "viewBox"
/\ joinWith " "
[ show $ x box.position
, show $ y box.position
, show $ x box.size
, show $ y box.size
]

View file

@ -2,10 +2,16 @@ module LayoutLens.Data.Vec2 where
import LayoutLens.Prelude import LayoutLens.Prelude
import Data.Array.NonEmpty as NEA
import Partial.Unsafe (unsafePartial)
newtype Radians = Radians Number newtype Radians = Radians Number
data Vec2 = Vec2 Number Number data Vec2 = Vec2 Number Number
-- {{{ Base helpers -- {{{ Base helpers
-- | Multiply by the matrix
-- | cos Θ -sin Θ
-- | sin Θ cos Θ
rotateBy :: Radians -> Vec2 -> Vec2 rotateBy :: Radians -> Vec2 -> Vec2
rotateBy (Radians angle) (Vec2 x y) = Vec2 (x * c - y * s) (x * s + y * c) rotateBy (Radians angle) (Vec2 x y) = Vec2 (x * c - y * s) (x * s + y * c)
where where
@ -30,25 +36,46 @@ x (Vec2 x _) = x
y :: Vec2 -> Number y :: Vec2 -> Number
y (Vec2 _ y) = y y (Vec2 _ y) = y
origin :: Vec2
origin = Vec2 0.0 0.0
-- }}} -- }}}
-- {{{ Shapes -- {{{ Shapes
newtype AABB = AABB { position :: Vec2, size :: Vec2 } newtype AABB = AABB { position :: Vec2, size :: Vec2 }
newtype Polygon = Polygon (Array Vec2) newtype Polygon = Polygon (NonEmptyArray Vec2)
aabbToPolygon :: AABB -> Polygon aabbToPolygon :: AABB -> Polygon
aabbToPolygon (AABB aabb@{ size: Vec2 sx sy }) = Polygon aabbToPolygon (AABB aabb@{ size: Vec2 sx sy }) = Polygon
$ unsafePartial -- Safe because the array always has four elements
$ fromJust
$ NEA.fromArray
[ aabb.position [ aabb.position
, aabb.position <> Vec2 0.0 sy , aabb.position <> Vec2 0.0 sy
, aabb.position <> aabb.size , aabb.position <> aabb.size
, aabb.position <> Vec2 sx 0.0 , aabb.position <> Vec2 sx 0.0
] ]
-- | The left-inverse of `aabbToPolygon`
boundingBox :: Polygon -> AABB
boundingBox (Polygon points) = AABB
{ position: Vec2 minX minY
, size: Vec2 (maxX - minX) (maxY - minY)
}
where
minX = NEA.foldl1 min $ x <$> points
minY = NEA.foldl1 min $ y <$> points
maxX = NEA.foldl1 max $ x <$> points
maxY = NEA.foldl1 max $ y <$> points
mapPoints :: (Vec2 -> Vec2) -> (Polygon -> Polygon) mapPoints :: (Vec2 -> Vec2) -> (Polygon -> Polygon)
mapPoints f (Polygon points) = Polygon $ f <$> points mapPoints f (Polygon points) = Polygon $ f <$> points
aabbCenter :: AABB -> Vec2 aabbCenter :: AABB -> Vec2
aabbCenter (AABB aabb) = aabb.position <> vscale 0.5 aabb.size aabbCenter (AABB aabb) = aabb.position <> vscale 0.5 aabb.size
originAabb :: Vec2 -> AABB
originAabb size = AABB { position: origin, size }
-- }}} -- }}}
-- {{{ Transforms -- {{{ Transforms
newtype RawScalePreservingTransform = RawScalePreservingTransform newtype RawScalePreservingTransform = RawScalePreservingTransform
@ -62,24 +89,72 @@ newtype ScalePreservingTransform = ScalePreservingTransform
, rotation :: Radians , rotation :: Radians
} }
normalizeScalePreservingTransform newtype Transform = Transform
:: RawScalePreservingTransform -> ScalePreservingTransform { scale :: Number
normalizeScalePreservingTransform (RawScalePreservingTransform t) = ScalePreservingTransform , position :: Vec2
{ rotation: t.rotateBy , rotation :: Radians
, position: t.rotateAround <> rotateBy t.rotateBy (vsub t.position t.rotateAround)
} }
-- | Intuitivey, the raw transformation has form
-- | v ↦ r_Θ(v + a) - a + p,
-- | but rotations are linear, so the above can be rewritten as
-- | v ↦ r_Θ(v) + (r_Θ(a) - a + p),
-- | which is what this function does.
-- |
-- | The other case works similarly
normalizeScalePreservingTransform
:: RawScalePreservingTransform -> ScalePreservingTransform
normalizeScalePreservingTransform (RawScalePreservingTransform t) =
ScalePreservingTransform
{ rotation: t.rotateBy
, position: t.rotateAround
<> rotateBy t.rotateBy (t.position <> vinv t.rotateAround)
}
-- | We want to compose
-- | f(v) = r_Θ(v) + p_f
-- | s(v) = r_ϕ(v) + p_s,
-- | which yields
-- | (s ∘ f)(v) = r_ϕ(r_Θ(v) + p_f) + p_s
-- | = r_ϕ(r_Θ(v)) + r_ϕ(p_f) + p_s
-- | = r_(ϕ+Θ)(v) + (r_ϕ(p_f) + p_s)
composeScalePreservingTransforms composeScalePreservingTransforms
:: ScalePreservingTransform -> ScalePreservingTransform -> ScalePreservingTransform :: ScalePreservingTransform -> ScalePreservingTransform -> ScalePreservingTransform
composeScalePreservingTransforms (ScalePreservingTransform first) (ScalePreservingTransform second) = composeScalePreservingTransforms (ScalePreservingTransform second) (ScalePreservingTransform first) =
ScalePreservingTransform ScalePreservingTransform
{ rotation: first.rotation <> second.rotation { rotation: first.rotation <> second.rotation
, position: second.position <> rotateBy second.rotation first.position , position: second.position <> rotateBy second.rotation first.position
} }
forgetScalePreservingStructure :: ScalePreservingTransform -> Transform
forgetScalePreservingStructure (ScalePreservingTransform transform) =
Transform
{ position: transform.position
, rotation: transform.rotation
, scale: 1.0
}
applyScalePreservingTransform :: ScalePreservingTransform -> Vec2 -> Vec2 applyScalePreservingTransform :: ScalePreservingTransform -> Vec2 -> Vec2
applyScalePreservingTransform (ScalePreservingTransform transform) v = applyScalePreservingTransform = forgetScalePreservingStructure >>> applyTransform
rotateBy transform.rotation v <> transform.position
applyTransform :: Transform -> Vec2 -> Vec2
applyTransform (Transform transform) v =
-- Rotations are linear, thus they commute with scaling.
vscale transform.scale (rotateBy transform.rotation v) <> transform.position
tScale :: Number -> Transform
tScale factor = Transform
{ scale: factor
, position: origin
, rotation: mempty
}
tTranslate :: Vec2 -> Transform
tTranslate position = Transform
{ scale: 1.0
, rotation: mempty
, position
}
-- }}} -- }}}
@ -89,12 +164,14 @@ derive instance Eq AABB
derive instance Eq Polygon derive instance Eq Polygon
derive instance Eq RawScalePreservingTransform derive instance Eq RawScalePreservingTransform
derive instance Eq ScalePreservingTransform derive instance Eq ScalePreservingTransform
derive instance Eq Transform
derive instance Generic Vec2 _ derive instance Generic Vec2 _
derive instance Generic Radians _ derive instance Generic Radians _
derive instance Generic AABB _ derive instance Generic AABB _
derive instance Generic Polygon _ derive instance Generic Polygon _
derive instance Generic RawScalePreservingTransform _ derive instance Generic RawScalePreservingTransform _
derive instance Generic ScalePreservingTransform _ derive instance Generic ScalePreservingTransform _
derive instance Generic Transform _
instance Debug Vec2 where instance Debug Vec2 where
debug = genericDebug debug = genericDebug
@ -114,6 +191,9 @@ instance Debug RawScalePreservingTransform where
instance Debug ScalePreservingTransform where instance Debug ScalePreservingTransform where
debug = genericDebug debug = genericDebug
instance Debug Transform where
debug = genericDebug
instance Semigroup Vec2 where instance Semigroup Vec2 where
append (Vec2 a b) (Vec2 c d) = Vec2 (a + c) (b + d) append (Vec2 a b) (Vec2 c d) = Vec2 (a + c) (b + d)
@ -128,4 +208,4 @@ instance Monoid Vec2 where
instance Monoid Radians where instance Monoid Radians where
mempty = Radians 0.0 mempty = Radians 0.0
derive newtype instance Monoid Polygon derive instance Newtype Radians _

View file

@ -0,0 +1,29 @@
module LayoutLens.Generate.Svg where
import LayoutLens.Prelude
import LayoutLens.Data.Config as C
import LayoutLens.Data.Geometry (Attribute(..))
import LayoutLens.Data.Geometry as G
import LayoutLens.Data.Svg as S
import LayoutLens.Data.Vec2 (forgetScalePreservingStructure, originAabb, tScale, vscale)
type SvgString = String
renderPhysicalKey :: C.PhysicalKey -> G.Attributes -> G.Geometry
renderPhysicalKey (C.PhysicalKey key) attributes = do
G.Transform (forgetScalePreservingStructure key.transform)
$ G.Rect (originAabb key.size) attributes
renderPhysicalLayout :: C.PhysicalLayout -> G.Geometry
renderPhysicalLayout (C.PhysicalLayout layout) =
G.Transform (tScale 100.0)
$ G.Many
$ flip renderPhysicalKey attributes
<$> layout
where
attributes =
[ Fill $ rgb 200 200 50
, Stroke black
, StrokeWidth 0.1
]

View file

@ -2,16 +2,28 @@ module Main where
import LayoutLens.Prelude import LayoutLens.Prelude
import LayoutLens.Data.Config (PhysicalLayout(..), buildConfig, buildPhysical) import LayoutLens.Data.Config (LensConfig(..), buildConfig)
import LayoutLens.Data.RawConfig (RawConfig(..)) import LayoutLens.Data.Geometry (pad)
import LayoutLens.Data.Svg (makeSvgDocument)
import LayoutLens.Data.Vec2 (Vec2(..))
import LayoutLens.Generate.Svg (renderPhysicalLayout)
import LayoutLens.Parser (parseConfig) import LayoutLens.Parser (parseConfig)
import Node.Encoding (Encoding(..)) import Node.Encoding (Encoding(..))
import Node.FS.Aff (readTextFile) import Node.FS.Aff (readTextFile, writeTextFile)
main :: Effect Unit main :: Effect Unit
main = launchAff_ do main = launchAff_ do
file <- readTextFile UTF8 "../keyboards/qmk/ferris-sweep/config.lens" file <- readTextFile UTF8 "../keyboards/qmk/ferris-sweep/config.lens"
-- file <- readTextFile UTF8 "./input.lens"
case parseConfig file of case parseConfig file of
Left err -> log err Left err -> log err
Right result -> do Right result -> do
logPretty $ buildConfig result let (LensConfig config) = buildConfig result
logPretty config
-- logPretty $ boundingPolygon $ renderPhysicalLayout config.physical
writeTextFile UTF8 "./output.svg"
$ makeSvgDocument
$ pad (Vec2 30.0 30.0)
$ renderPhysicalLayout
$ config.physical

View file

@ -90,6 +90,7 @@ name = ows *> P.try do
, "section" , "section"
, "layer" , "layer"
, "block" , "block"
, "pre"
, "end" , "end"
, "point" , "point"
, "place" , "place"
@ -151,7 +152,12 @@ physical = do
angle <- radians angle <- radians
around <- P.option position vec2 around <- P.option position vec2
pure $ angle /\ around pure $ angle /\ around
pure $ Place $ RawScalePreservingTransform { position, rotateBy, rotateAround } pure $ Place $
RawScalePreservingTransform
{ position
, rotateBy
, rotateAround
}
point :: Parser RawPhysicalActionStep point :: Parser RawPhysicalActionStep
point = do point = do
@ -163,7 +169,11 @@ physical = do
let rotateAround = position let rotateAround = position
let let
point a b c d = Point point a b c d = Point
{ transform: RawScalePreservingTransform { position: a, rotateBy: b, rotateAround: c } { transform: RawScalePreservingTransform
{ position: a
, rotateBy: b
, rotateAround: c
}
, size: d , size: d
} }
case arguments of case arguments of

View file

@ -26,6 +26,7 @@ module LayoutLens.Prelude
, module Data.Monoid.Generic , module Data.Monoid.Generic
, module Data.String , module Data.String
, module Data.List , module Data.List
, module Data.Array.NonEmpty
, wrapInto , wrapInto
, unimplemented , unimplemented
, logPretty , logPretty
@ -62,6 +63,7 @@ import Effect.Class.Console (clear, group, groupCollapsed, groupEnd, grouped, in
import Effect.Exception.Unsafe (unsafeThrow) import Effect.Exception.Unsafe (unsafeThrow)
import Prim.TypeError (class Warn, Text) import Prim.TypeError (class Warn, Text)
import Safe.Coerce (class Coercible, coerce) import Safe.Coerce (class Coercible, coerce)
import Data.Array.NonEmpty (NonEmptyArray)
unimplemented :: forall a. Warn (Text "unimplemenet") => a unimplemented :: forall a. Warn (Text "unimplemenet") => a
unimplemented = unsafeThrow "unimplemented" unimplemented = unsafeThrow "unimplemented"

View file

@ -0,0 +1 @@
au BufNewFile,BufRead *.lens set filetype=lens

View file

@ -1,12 +1,12 @@
" if exists("b:current_syntax") if exists("b:current_syntax")
" finish finish
" endif endif
set iskeyword+=- set iskeyword+=-
syntax keyword lensKeyword physical section layergroup layer chordgroup block end syntax keyword lensKeyword physical section layergroup layer chordgroup block end
syntax keyword lensAction sticky-switch switch syntax keyword lensAction sticky-switch switch
syntax keyword lensFunction columns place action key syntax keyword lensFunction columns place action key after before
syntax keyword lensLayerName center topleft topright bottomleft bottomright syntax keyword lensLayerName center topleft topright bottomleft bottomright
syntax match lensComment "\v--.*$" syntax match lensComment "\v--.*$"