Skip to content
This repository has been archived by the owner on Jun 18, 2023. It is now read-only.

Commit

Permalink
feat(client): working basic renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
prescientmoon committed Oct 19, 2020
1 parent 72068c9 commit 6f677f1
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 32 deletions.
44 changes: 28 additions & 16 deletions packages/client/src/Main.purs
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
module Main where

import Prelude
import Data.Maybe (Maybe(..))
import Data.List as List
import Data.Maybe (Maybe(..), fromJust)
import Data.Tuple (fst)
import Debug.Trace (traceM)
import Effect (Effect)
import Effect.Console as Console
import Graphics.Canvas (getCanvasElementById, getContext2D)
import Lunarbox.Render (render, runRenderM)
import Lunarflow.Ast (withDebrujinIndices)
import Lunarflow.Ast.Grouped (groupExpression)
import Lunarflow.Geometry.Foreign (Geometry, fromShape, renderGeometry)
import Lunarflow.Geometry.Types (PolygonAttribs(..), circle, group, polygon, rect)
import Lunarflow.Layout (addIndices, fromScoped, runLayoutM)
import Lunarflow.Parser (unsafeParseLambdaCalculus)
import Lunarflow.Renderer.WithHeight (withHeights)
import Partial.Unsafe (unsafePartial)

geometry :: Geometry
geometry =
fromShape
$ group { stroke: "red" }
[ polygon { fill: "brown" }
$ PolygonAttribs
[ { x: 100, y: 110 }
, { x: 140, y: 70 }
, { x: 200, y: 190 }
, { x: 350, y: 170 }
, { x: 190, y: 400 }
, { x: 100, y: 370 }
]
, rect { fill: "yellow" } { x: 200, y: 40 } 70 160
, circle { fill: "green", stroke: "transparent" } { x: 300, y: 300 } 70
]
fromShape $ runRenderM
$ render
$ fst
$ withHeights
$ unsafePartial
$ fromJust
-- TODO: fix bug with application creating a line in the same place as the lambda used as argument.

$ flip List.index 1
$ List.catMaybes
$ map fromScoped
$ runLayoutM
$ addIndices
$ groupExpression
$ withDebrujinIndices
$ unsafeParseLambdaCalculus """\f a b -> f b a \x -> x"""

main :: Effect Unit
main = do
Expand All @@ -32,4 +43,5 @@ main = do
Nothing -> Console.log "No canvas found"
Just canvas' -> do
ctx <- getContext2D canvas'
traceM geometry
renderGeometry geometry ctx
107 changes: 91 additions & 16 deletions packages/client/src/Render.purs
Original file line number Diff line number Diff line change
@@ -1,43 +1,118 @@
module Lunarbox.Render where

import Prelude
import Data.FoldableWithIndex (foldrWithIndex)
import Data.List as List
import Lunarflow.Ast (AstF(..))
import Lunarflow.Geometry.Foreign (fitIntoBounds)
import Data.Set as Set
import Data.Tuple (Tuple(..))
import Debug.Trace (traceM)
import Lunarflow.Ast (AstF(..), isVar)
import Lunarflow.Geometry.Foreign (getRightBound)
import Lunarflow.Geometry.Foreign as ForeignShape
import Lunarflow.Geometry.Types as Shape
import Lunarflow.Layout (Layout, LayoutF)
import Matryoshka (Algebra, cata)
import Run (Run)
import Run.Reader (READER, local)
import Lunarflow.Geometry.Utils (withPadding)
import Lunarflow.Renderer.WithHeight (YLayout, YLayoutF, YMapSlice, getPosition)
import Matryoshka (GAlgebra, para)
import Run (Run, extract)
import Run.Reader (READER, ask, local, runReader)

type RenderContext
= { doNotRender :: List.List Int
= { doNotRender :: Set.Set Int
, start :: Int
, end :: Int
, slice :: YMapSlice
}

type RenderM r
= Run ( reader :: READER RenderContext | r )

-- | Prepare stuff for rendering inside a lambda.
shiftContext :: Int -> RenderContext -> RenderContext
shiftContext by ctx = ctx { doNotRender = ((+) by) <$> ctx.doNotRender }
shiftContext by ctx = ctx { doNotRender = ((+) by) `Set.map` ctx.doNotRender }

lineHeight :: Int
lineHeight = 50

lineWidth :: Int
lineWidth = 100

{--
So:
- When we encounter a lambda, we draw the body and then the box around it
- When we encouner a var, we check where it is in scope and draw until here
--}
render :: forall r. Layout -> RenderM r Shape.Shape
render = cata algebra
render :: forall r. YLayout -> RenderM r Shape.Shape
render = para algebra
where
algebra :: Algebra LayoutF (RenderM r Shape.Shape)
algebra (Lambda { args } body) = do
bodyShape <- local (shiftContext $ List.length args) body
algebra :: GAlgebra (Tuple YLayout) YLayoutF (RenderM r Shape.Shape)
algebra (Lambda { args, heights } (Tuple _ body)) = do
bodyShape <- local (_ { slice = heights } <<< (shiftContext $ List.length args)) body
let
bounds = fitIntoBounds bodyShape
bounds = ForeignShape.bounds bodyShape
pure
$ Shape.group {}
[ Shape.fromBounds { fill: "transparent", stroke: "red" } bounds
[ Shape.rect { fill: "black", stroke: "red" } $ withPadding 20 bounds
, bodyShape
]

algebra _ = pure mempty
algebra (Var { position, index }) = do
{ end, start, slice, doNotRender } <- ask
if (Set.member index doNotRender) then
pure mempty
else do
pure
$ Shape.rect { fill: "yellow", stroke: "black" }
{ x: 0
, y: getY position slice
, height: lineHeight
, width: max lineWidth start
}

algebra (Call position mkFunc@(Tuple functionLayout _) mkArg@(Tuple argumentLayout _)) = do
function <- renderFn 0 mkFunc
slice <- ask <#> _.slice
traceM ("Call position " <> show position)
let
functionEnd = if isVar functionLayout then 0 else getRightBound function

functionPosition = getPosition functionLayout
argument <- renderFn functionEnd mkArg
let
argumentEnd = getRightBound argument
pure
$ Shape.group {}
[ function
, argument
, Shape.rect
{ fill: "green"
, stroke: "black"
}
{ x: functionEnd
, width: argumentEnd - functionEnd
, height: lineHeight
, y: getY functionPosition slice
}
, Shape.rect
{ fill: "blue"
, stroke: "black"
}
{ x: argumentEnd
, width: lineWidth
, height: lineHeight
, y: getY position slice
}
]
where
renderFn start (Tuple ast m) = local (_ { start = start, end = start + lineWidth }) m

getY :: Int -> YMapSlice -> Int
getY position slice =
lineHeight
* foldrWithIndex
(\index height result -> if position < index then result else result + height)
0
slice

-- | Run a computation in the Render monad
runRenderM :: forall a. RenderM () a -> a
runRenderM m = extract $ runReader { doNotRender: Set.empty, start: 0, end: 0, slice: mempty } m
100 changes: 100 additions & 0 deletions packages/client/src/Renderer/WithHeight.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module Lunarflow.Renderer.WithHeight
( YLayoutF
, YLayout
, YMapSlice
, withHeights
, getPosition
) where

import Prelude
import Data.Array (foldr)
import Data.Array as Array
import Data.Bifunctor (lmap)
import Data.Foldable (sum)
import Data.Functor.Mu (Mu)
import Data.List (List, length) as List
import Data.Map as Map
import Data.Tuple (Tuple(..), snd, uncurry)
import Lunarflow.Array (maxZip)
import Lunarflow.Ast (AstF(..), call, lambda, var)
import Lunarflow.Label (class Label)
import Lunarflow.Layout (Layout, LayoutF)
import Matryoshka (Algebra, cata, project)

-- | The base functor for YLayouts.
type YLayoutF
= AstF { position :: Int, index :: Int } Int { position :: Int, args :: List.List Int, heights :: YMapSlice }

-- | YLayouts are layouts which keep track of the maximum height of each line
type YLayout
= Mu YLayoutF

-- TODO: make this a newtype with semigruop (& monoid?) instances
-- | A slice is just an array where each element
-- | represents the height of the line at the same (local) y index
type YMapSlice
= Array Int

-- TODO: make this a newtype with semigruop & monoid instances.
-- | Map from de brujin indices to slices
type YMap
= Map.Map Int YMapSlice

-- | Add height data to a layout.
withHeights ::
Layout ->
Tuple YLayout YMap
withHeights = cata algebra
where
algebra :: Algebra LayoutF (Tuple YLayout YMap)
algebra = case _ of
Lambda { position, args } (Tuple body bodyMeasures) -> Tuple yLambda bodyMeasures
where
(Tuple slice remaining) = splitMap (List.length args) bodyMeasures

measures = updateMap 0 position (sum slice) remaining

yLambda = lambda { position, args, heights: slice } body
Var { index, position } -> Tuple yVar yMap
where
yVar = var { index, position }

yMap = updateMap index position 1 Map.empty
Call position (Tuple function functionMeasures) (Tuple argument argumentMeasures) -> Tuple yCall yMap
where
yCall = call position function argument

yMap = updateMap 0 position 1 $ mergeYMaps functionMeasures argumentMeasures

-- | Update an ymap at an arbitrary index, position and value.
updateMap :: (Label "index" => Int) -> (Label "position" => Int) -> (Label "value" => Int) -> YMap -> YMap
updateMap index position value = mergeYMaps $ Map.singleton index $ Array.snoc (Array.replicate position 1) value

-- | Pretty much a semigroup implementation for YMaps
mergeYMaps :: YMap -> YMap -> YMap
mergeYMaps = Map.unionWith mergeArrays

-- | Pretty much a semigroup implementation for YMapSlice
mergeArrays :: Array Int -> Array Int -> Array Int
mergeArrays arr arr' = maxZip 1 1 arr arr' <#> uncurry max

splitMap :: Int -> YMap -> Tuple YMapSlice YMap
splitMap shiftBy yMap = Tuple slice remaining
where
slice = foldr mergeArrays [] $ snd <$> split.yes

remaining = Map.fromFoldable $ lmap (_ - shiftBy) <$> split.no

split = flip Array.partition arr \(Tuple x _) -> x < shiftBy

arr :: Array _
arr = Map.toUnfoldable yMap

-- | Get the position an YLayout is at.
getPosition :: YLayout -> Int
getPosition =
project
>>> case _ of
Var { position } -> position
Call position _ _ -> position
Lambda { position } _ -> position

0 comments on commit 6f677f1

Please sign in to comment.