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:
debugged:
git: https://github.com/mateiadrielrafael/purescript-debugged.git
ref: 0d5a4149279129f10c8fe2a3ef280b9fde4d5116
ref: 70d4b2e19c8831753a62a4527b0c8c40a25b2a4e
packages:
aff:
type: registry
@ -694,7 +694,7 @@ packages:
debugged:
type: git
url: https://github.com/mateiadrielrafael/purescript-debugged.git
rev: 0d5a4149279129f10c8fe2a3ef280b9fde4d5116
rev: 70d4b2e19c8831753a62a4527b0c8c40a25b2a4e
dependencies:
- arrays
- bifunctors

View file

@ -23,4 +23,5 @@ workspace:
extra_packages:
debugged:
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 transform (PhysicalKey key) = PhysicalKey
{ size: key.size
, transform: composeScalePreservingTransforms key.transform transform
, transform: composeScalePreservingTransforms transform key.transform
}
buildPhysical :: RawPhysical -> PhysicalLayout

View file

@ -3,19 +3,21 @@ module LayoutLens.Data.Geometry where
import LayoutLens.Prelude
import Data.Array as Array
import LayoutLens.Data.Vec2 as V
import LayoutLens.Data.Vec2
( AABB(..)
, Polygon(..)
, ScalePreservingTransform
, Vec2(..)
, aabbToPolygon
, applyScalePreservingTransform
, applyTransform
, boundingBox
, mapPoints
, tTranslate
, vinv
, vscale
)
data Attribute = Fill Color | Stroke Color
data Attribute = Fill Color | Stroke Color | StrokeWidth Number
type Attributes = Array Attribute
type GenericAttributes = Array (String /\ String)
@ -31,28 +33,30 @@ data PathStep
| Close
data Geometry
= Transform ScalePreservingTransform Geometry
= Transform V.Transform Geometry
| Many (Array Geometry)
| Text GenericAttributes Attributes String
| Rect AABB Attributes
| Path (Array PathStep) Attributes
| Invisible Geometry
-- Approximate the size of some geometry by fitting a polygon around it
boundingPolygon :: Geometry -> Polygon
boundingPolygon :: Geometry -> Maybe Polygon
boundingPolygon = case _ of
Rect aabb _ -> aabbToPolygon aabb
Text _ _ _ -> mempty
Rect aabb _ -> pure $ aabbToPolygon aabb
Text _ _ _ -> Nothing
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
where
points :: Vec2 -> PathStep -> Vec2 /\ Polygon
points :: Vec2 -> PathStep -> Vec2 /\ Maybe Polygon
points prev = case _ of
Close -> prev /\ Polygon []
MoveTo a -> a /\ Polygon [ a ]
LineTo a -> a /\ Polygon [ a ]
Close -> prev /\ Nothing
MoveTo a -> a /\ (pure $ Polygon $ pure a)
LineTo a -> a /\ (pure $ Polygon $ pure a)
-- 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
aabb = AABB
{ position: center <> vinv diagonal
@ -62,6 +66,21 @@ boundingPolygon = case _ of
diagonal = Vec2 arc.radius arc.radius
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 PathStep
derive instance Eq Geometry

View file

@ -4,20 +4,24 @@ import LayoutLens.Prelude
import Data.Array as Array
import Data.String (Pattern(..))
import LayoutLens.Data.Geometry (Geometry(..), Attribute(..), Attributes, GenericAttributes, PathStep(..))
import LayoutLens.Data.Vec2 (AABB(..), ScalePreservingTransform(..), Vec2(..), radiansToDegrees, x, y)
import LayoutLens.Data.Geometry (Attribute(..), Attributes, GenericAttributes, Geometry(..), PathStep(..), boundingPolygon)
import LayoutLens.Data.Vec2 (AABB(..), Vec2(..), boundingBox, x, y)
import LayoutLens.Data.Vec2 as V
indent :: String -> String
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 name attributes child = fold
[ "<"
, name
, " "
, joinWith " "
$ uncurry (\key value -> fold [ key, "=\"", value, "\"" ])
<$> attributes
, printAttributes attributes
, ">"
, indent child
, "</"
@ -26,12 +30,19 @@ tag name attributes child = fold
]
leaf :: String -> GenericAttributes -> String
leaf name attributes = tag name attributes mempty
leaf name attributes = fold
[ "<"
, name
, " "
, printAttributes attributes
, "/>"
]
printGeometryAttributes :: Attributes -> GenericAttributes
printGeometryAttributes = map case _ of
Fill color -> "fill" /\ toHexString color
Stroke color -> "stroke" /\ toHexString color
StrokeWidth number -> "stroke-width" /\ show number
px :: Number -> String
px n = show n <> "px"
@ -39,6 +50,7 @@ px n = show n <> "px"
-- Render a geometry to svg
renderGeometry :: Geometry -> String
renderGeometry = case _ of
Invisible _ -> ""
Many array -> unlines $ renderGeometry <$> array
Rect (AABB aabb) proper ->
@ -47,8 +59,8 @@ renderGeometry = case _ of
<>
[ "x" /\ show (x aabb.position)
, "y" /\ show (y aabb.position)
, "width" /\ px (x aabb.size)
, "height" /\ px (y aabb.size)
, "width" /\ show (x aabb.size)
, "height" /\ show (y aabb.size)
]
Path steps proper ->
@ -78,12 +90,37 @@ renderGeometry = case _ of
(generic <> printGeometryAttributes proper)
string
Transform (ScalePreservingTransform transform) g ->
Transform (V.Transform transform) g ->
tag "g"
[ "transform" /\ fold
[ "rotate("
, show $ radiansToDegrees transform.rotation
[ "transform" /\ joinWith " "
[ "matrix("
, 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
-- | 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 Data.Array.NonEmpty as NEA
import Partial.Unsafe (unsafePartial)
newtype Radians = Radians Number
data Vec2 = Vec2 Number Number
-- {{{ Base helpers
-- | Multiply by the matrix
-- | cos Θ -sin Θ
-- | sin Θ cos Θ
rotateBy :: Radians -> Vec2 -> Vec2
rotateBy (Radians angle) (Vec2 x y) = Vec2 (x * c - y * s) (x * s + y * c)
where
@ -30,25 +36,46 @@ x (Vec2 x _) = x
y :: Vec2 -> Number
y (Vec2 _ y) = y
origin :: Vec2
origin = Vec2 0.0 0.0
-- }}}
-- {{{ Shapes
newtype AABB = AABB { position :: Vec2, size :: Vec2 }
newtype Polygon = Polygon (Array Vec2)
newtype Polygon = Polygon (NonEmptyArray Vec2)
aabbToPolygon :: AABB -> 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 <> Vec2 0.0 sy
, aabb.position <> aabb.size
, 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 f (Polygon points) = Polygon $ f <$> points
aabbCenter :: AABB -> Vec2
aabbCenter (AABB aabb) = aabb.position <> vscale 0.5 aabb.size
originAabb :: Vec2 -> AABB
originAabb size = AABB { position: origin, size }
-- }}}
-- {{{ Transforms
newtype RawScalePreservingTransform = RawScalePreservingTransform
@ -62,24 +89,72 @@ newtype ScalePreservingTransform = ScalePreservingTransform
, rotation :: Radians
}
normalizeScalePreservingTransform
:: RawScalePreservingTransform -> ScalePreservingTransform
normalizeScalePreservingTransform (RawScalePreservingTransform t) = ScalePreservingTransform
{ rotation: t.rotateBy
, position: t.rotateAround <> rotateBy t.rotateBy (vsub t.position t.rotateAround)
newtype Transform = Transform
{ scale :: Number
, position :: Vec2
, rotation :: Radians
}
-- | 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
:: ScalePreservingTransform -> ScalePreservingTransform -> ScalePreservingTransform
composeScalePreservingTransforms (ScalePreservingTransform first) (ScalePreservingTransform second) =
composeScalePreservingTransforms (ScalePreservingTransform second) (ScalePreservingTransform first) =
ScalePreservingTransform
{ rotation: first.rotation <> second.rotation
, 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 transform) v =
rotateBy transform.rotation v <> transform.position
applyScalePreservingTransform = forgetScalePreservingStructure >>> applyTransform
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 RawScalePreservingTransform
derive instance Eq ScalePreservingTransform
derive instance Eq Transform
derive instance Generic Vec2 _
derive instance Generic Radians _
derive instance Generic AABB _
derive instance Generic Polygon _
derive instance Generic RawScalePreservingTransform _
derive instance Generic ScalePreservingTransform _
derive instance Generic Transform _
instance Debug Vec2 where
debug = genericDebug
@ -114,6 +191,9 @@ instance Debug RawScalePreservingTransform where
instance Debug ScalePreservingTransform where
debug = genericDebug
instance Debug Transform where
debug = genericDebug
instance Semigroup Vec2 where
append (Vec2 a b) (Vec2 c d) = Vec2 (a + c) (b + d)
@ -128,4 +208,4 @@ instance Monoid Vec2 where
instance Monoid Radians where
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.Data.Config (PhysicalLayout(..), buildConfig, buildPhysical)
import LayoutLens.Data.RawConfig (RawConfig(..))
import LayoutLens.Data.Config (LensConfig(..), buildConfig)
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 Node.Encoding (Encoding(..))
import Node.FS.Aff (readTextFile)
import Node.FS.Aff (readTextFile, writeTextFile)
main :: Effect Unit
main = launchAff_ do
file <- readTextFile UTF8 "../keyboards/qmk/ferris-sweep/config.lens"
-- file <- readTextFile UTF8 "./input.lens"
case parseConfig file of
Left err -> log err
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"
, "layer"
, "block"
, "pre"
, "end"
, "point"
, "place"
@ -151,7 +152,12 @@ physical = do
angle <- radians
around <- P.option position vec2
pure $ angle /\ around
pure $ Place $ RawScalePreservingTransform { position, rotateBy, rotateAround }
pure $ Place $
RawScalePreservingTransform
{ position
, rotateBy
, rotateAround
}
point :: Parser RawPhysicalActionStep
point = do
@ -163,7 +169,11 @@ physical = do
let rotateAround = position
let
point a b c d = Point
{ transform: RawScalePreservingTransform { position: a, rotateBy: b, rotateAround: c }
{ transform: RawScalePreservingTransform
{ position: a
, rotateBy: b
, rotateAround: c
}
, size: d
}
case arguments of

View file

@ -26,6 +26,7 @@ module LayoutLens.Prelude
, module Data.Monoid.Generic
, module Data.String
, module Data.List
, module Data.Array.NonEmpty
, wrapInto
, unimplemented
, logPretty
@ -62,6 +63,7 @@ import Effect.Class.Console (clear, group, groupCollapsed, groupEnd, grouped, in
import Effect.Exception.Unsafe (unsafeThrow)
import Prim.TypeError (class Warn, Text)
import Safe.Coerce (class Coercible, coerce)
import Data.Array.NonEmpty (NonEmptyArray)
unimplemented :: forall a. Warn (Text "unimplemenet") => a
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")
" finish
" endif
if exists("b:current_syntax")
finish
endif
set iskeyword+=-
syntax keyword lensKeyword physical section layergroup layer chordgroup block end
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 match lensComment "\v--.*$"