diff --git a/packages/client/src/Main.purs b/packages/client/src/Main.purs index 60a6083..44f384d 100644 --- a/packages/client/src/Main.purs +++ b/packages/client/src/Main.purs @@ -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 @@ -32,4 +43,5 @@ main = do Nothing -> Console.log "No canvas found" Just canvas' -> do ctx <- getContext2D canvas' + traceM geometry renderGeometry geometry ctx diff --git a/packages/client/src/Render.purs b/packages/client/src/Render.purs index 5871091..752b1b8 100644 --- a/packages/client/src/Render.purs +++ b/packages/client/src/Render.purs @@ -1,17 +1,26 @@ 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 @@ -19,25 +28,91 @@ type RenderM 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 diff --git a/packages/client/src/Renderer/WithHeight.purs b/packages/client/src/Renderer/WithHeight.purs new file mode 100644 index 0000000..06d0a06 --- /dev/null +++ b/packages/client/src/Renderer/WithHeight.purs @@ -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