mirror of
https://github.com/rgleichman/glance.git
synced 2024-11-26 17:14:21 +03:00
322 lines
13 KiB
Haskell
322 lines
13 KiB
Haskell
{-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts, TypeFamilies, PartialTypeSignatures, ScopedTypeVariables #-}
|
|
|
|
module Rendering (
|
|
renderDrawing,
|
|
customLayoutParams,
|
|
renderIngSyntaxGraph
|
|
) where
|
|
|
|
import Diagrams.Core.Names(Name(..))
|
|
import Diagrams.Prelude hiding ((#), (&))
|
|
import Diagrams.TwoD.GraphViz(mkGraph, getGraph, layoutGraph')
|
|
--import Diagrams.Backend.SVG(B)
|
|
|
|
import qualified Data.GraphViz as GV
|
|
import qualified Data.GraphViz.Attributes.Complete as GVA
|
|
import qualified Data.Map as Map
|
|
import Data.Maybe(isJust)
|
|
|
|
import Control.Arrow(second)
|
|
import Data.Function(on)
|
|
import qualified Data.Graph.Inductive as ING
|
|
import Data.Graph.Inductive.PatriciaTree (Gr)
|
|
import Data.List(minimumBy)
|
|
import Data.Typeable(Typeable)
|
|
|
|
--import qualified Data.GraphViz.Types
|
|
--import Data.GraphViz.Commands
|
|
--import qualified Debug.Trace
|
|
--import Data.Word(Word16)
|
|
|
|
import Icons(colorScheme, iconToDiagram, nameDiagram, defaultLineWidth, ColorStyle(..), TransformableDia)
|
|
import TranslateCore(nodeToIcon)
|
|
import Types(Edge(..), Icon, EdgeOption(..), Connection, Drawing(..), EdgeEnd(..),
|
|
NameAndPort(..), SpecialQDiagram, SpecialBackend, SyntaxNode)
|
|
import Util(fromMaybeError)
|
|
|
|
-- If the inferred types for these functions becomes unweildy,
|
|
-- try using PartialTypeSignitures.
|
|
|
|
-- CONSTANT
|
|
graphvizScaleFactor :: (Fractional a) => a
|
|
|
|
-- For Neato
|
|
graphvizScaleFactor = 0.12
|
|
|
|
-- For Fdp
|
|
--scaleFactor = 0.09
|
|
|
|
--scaleFactor = 0.04
|
|
|
|
drawingToGraphvizScaleFactor :: Double
|
|
-- For Neato, ScaleOverlaps
|
|
--drawingToGraphvizScaleFactor = 0.08
|
|
|
|
-- For Neato, PrismOverlap
|
|
drawingToGraphvizScaleFactor = 0.15
|
|
|
|
-- CONVERTING Edges AND Icons TO DIAGRAMS --
|
|
makeNamedMapFromGraph :: (SpecialBackend b, ING.Graph gr) =>
|
|
gr (d, Icon) e -> [(d, Icons.TransformableDia b)]
|
|
makeNamedMapFromGraph graph = (second iconToDiagram . snd) <$> ING.labNodes graph
|
|
|
|
-- Note that the name type alias is different from the Name constructor.
|
|
getTopLevelName :: Name -> Name
|
|
getTopLevelName (Name []) = Name []
|
|
getTopLevelName (Name (x:_)) = Name [x]
|
|
|
|
-- TODO Refactor with syntaxGraphToFglGraph in TranslateCore
|
|
drawingToIconGraph :: Drawing -> Gr (Name, Icon) Edge
|
|
drawingToIconGraph (Drawing nodes edges) =
|
|
mkGraph nodes labeledEdges where
|
|
labeledEdges = fmap makeLabeledEdge edges
|
|
makeLabeledEdge e@(Edge _ _ (NameAndPort n1 _, NameAndPort n2 _)) =
|
|
let name1 = getTopLevelName n1
|
|
name2 = getTopLevelName n2
|
|
in
|
|
((name1, lookupInNodes name1), (name2, lookupInNodes name2), e) where
|
|
lookupInNodes name = fromMaybeError errorString (lookup name nodes) where
|
|
errorString =
|
|
"syntaxGraphToFglGraph edge connects to non-existent node. Node Name ="
|
|
++ show name ++ " Edge=" ++ show e
|
|
|
|
|
|
-- | Custom arrow tail for the arg1 result circle.
|
|
-- The ArrowHT type does not seem to be documented.
|
|
arg1ResT :: (RealFloat n) => ArrowHT n
|
|
arg1ResT len _ = (alignR $ circle (len / 2), mempty)
|
|
|
|
-- | Arrow head version of arg1ResT
|
|
arg1ResH :: (RealFloat n) => ArrowHT n
|
|
arg1ResH len _ = (alignL $ circle (len / 2), mempty)
|
|
|
|
bezierShaft :: (V t ~ V2, TrailLike t) => Angle (N t) -> Angle (N t) -> t
|
|
bezierShaft angle1 angle2 = fromSegments [bezier3 c1 c2 x] where
|
|
scaleFactor = 0.5
|
|
x = r2 (1,0)
|
|
c1 = rotate angle1 (scale scaleFactor unitX)
|
|
c2 = rotate angle2 (scale scaleFactor unitX) ^+^ x
|
|
|
|
getArrowOpts :: (RealFloat n, Typeable n) => (EdgeEnd, EdgeEnd) -> [EdgeOption]-> ArrowOpts n
|
|
getArrowOpts (t, h) opts = arrowOptions
|
|
where
|
|
shaftColor = if EdgeInPattern `elem` opts then patternC else lineC
|
|
ap1ArgTexture = solid (backgroundC colorScheme)
|
|
ap1ArgStyle = lwG defaultLineWidth . lc (apply1C colorScheme)
|
|
ap1ResultTexture = solid (apply1C colorScheme)
|
|
|
|
lookupTail EndNone = id
|
|
lookupTail EndAp1Arg = (arrowTail .~ dart')
|
|
. (tailTexture .~ ap1ArgTexture) . (tailStyle %~ ap1ArgStyle)
|
|
lookupTail EndAp1Result = (arrowTail .~ arg1ResT) . (tailTexture .~ ap1ResultTexture)
|
|
|
|
lookupHead EndNone = id
|
|
lookupHead EndAp1Arg = (arrowHead .~ dart)
|
|
. (headTexture .~ ap1ArgTexture) . (headStyle %~ ap1ArgStyle)
|
|
lookupHead EndAp1Result = (arrowHead .~ arg1ResH) . (headTexture .~ ap1ResultTexture)
|
|
|
|
arrowOptions =
|
|
arrowHead .~ noHead $
|
|
arrowTail .~ noTail $
|
|
lengths .~ global 0.75 $
|
|
shaftStyle %~ (lwG defaultLineWidth . lc (shaftColor colorScheme)) $
|
|
lookupHead h $ lookupTail t with
|
|
|
|
-- | Given an Edge, return a transformation on Diagrams that will draw a line.
|
|
connectMaybePorts :: SpecialBackend b =>
|
|
Edge -> SpecialQDiagram b -> SpecialQDiagram b
|
|
connectMaybePorts (Edge opts ends (NameAndPort icon0 (Just port0), NameAndPort icon1 (Just port1))) =
|
|
connect'
|
|
(getArrowOpts ends opts)
|
|
(icon0 .> port0)
|
|
(icon1 .> port1)
|
|
connectMaybePorts (Edge opts ends (NameAndPort icon0 Nothing, NameAndPort icon1 (Just port1))) =
|
|
connectOutside' (getArrowOpts ends opts) icon0 (icon1 .> port1)
|
|
connectMaybePorts (Edge opts ends (NameAndPort icon0 (Just port0), NameAndPort icon1 Nothing)) =
|
|
connectOutside' (getArrowOpts ends opts) (icon0 .> port0) icon1
|
|
connectMaybePorts (Edge opts ends (NameAndPort icon0 Nothing, NameAndPort icon1 Nothing)) =
|
|
connectOutside' (getArrowOpts ends opts) icon0 icon1
|
|
|
|
makeConnections :: SpecialBackend b =>
|
|
[Edge] -> SpecialQDiagram b -> SpecialQDiagram b
|
|
makeConnections edges = applyAll connections
|
|
where
|
|
connections = map connectMaybePorts edges
|
|
|
|
-- ROTATING/FLIPPING ICONS --
|
|
|
|
--printSelf :: (Show a) => a -> a
|
|
--printSelf a = Debug.Trace.trace (show a ++ "/n") a
|
|
|
|
{-# ANN totalLenghtOfLines "HLint: ignore Redundant bracket" #-}
|
|
{-# ANN totalLenghtOfLines "HLint: ignore Move brackets to avoid $" #-}
|
|
-- | For a specific icon, given its angle, location, and a list of pairs of locations
|
|
-- of (this icon's port, icon that connects to this port), return the sum of the
|
|
-- distances (possibly squared) between the ports and the icons they connect to.
|
|
-- This function is used to find that angle that minimizes the sum of distances.
|
|
totalLenghtOfLines :: Double -> P2 Double -> [(P2 Double, P2 Double)] -> Double
|
|
totalLenghtOfLines angle myLocation edges = sum $ map edgeDist edges
|
|
where
|
|
edgeDist :: (P2 Double, P2 Double) -> Double
|
|
edgeDist (relativePortLocation, iconLocation) =
|
|
-- The squaring here is arbitrary. Distance should be replaced with angle diff.
|
|
(norm $ absPortVec ^-^ iconLocationVec) ** 2
|
|
where
|
|
P relPortVec = relativePortLocation
|
|
P iconLocationVec = iconLocation
|
|
P myLocVec = myLocation
|
|
absPortVec = myLocVec ^+^ (rotateBy angle relPortVec)
|
|
|
|
-- | For a specific icon, given its location, and a list of pairs of locations
|
|
-- of (this icon's port, icon that connects to this port), find the angle that
|
|
-- minimizes the the sum of the distances (possibly squared) between the ports
|
|
-- and the icons they connect to. Returns (angle, sum of distances).
|
|
-- todo: Return 0 immediatly if edges == [].
|
|
angleWithMinDist :: P2 Double -> [(P2 Double, P2 Double)] -> (Double, Double)
|
|
angleWithMinDist myLocation edges =
|
|
minimumBy (compare `on` snd) $ map totalLength [0,(1/40)..1]
|
|
where
|
|
totalLength angle = (angle, totalLenghtOfLines angle myLocation edges)
|
|
|
|
getFromMapAndScale :: (Fractional a, Functor f, Ord k) => Map.Map k (f a) -> k -> f a
|
|
getFromMapAndScale posMap name = graphvizScaleFactor *^ (posMap Map.! name)
|
|
|
|
-- | Returns [(myport, other node, maybe other node's port)]
|
|
connectedPorts :: [Connection] -> Name -> [(Int, Name, Maybe Int)]
|
|
connectedPorts edges name = map edgeToPort $ filter nameInEdge edges
|
|
where
|
|
isPort = isJust
|
|
nameInEdge (NameAndPort name1 port1, NameAndPort name2 port2) = (name == name1 && isPort port1) || (name == name2 && isPort port2)
|
|
edgeToPort (NameAndPort name1 port1, NameAndPort name2 port2) =
|
|
if name == name1
|
|
then (fromMaybeError "connectedPorts: port is Nothing" port1, name2, port2)
|
|
else (fromMaybeError "connectedPorts: port is Nothing" port2, name1, port1)
|
|
|
|
-- | rotateNodes rotates the nodes such that the distance of its connecting lines
|
|
-- are minimized.
|
|
-- Precondition: the diagrams are already centered
|
|
-- todo: confirm precondition (or use a newtype)
|
|
rotateNodes ::
|
|
Map.Map Name (Point V2 Double)
|
|
-> [(Name, Bool -> Double -> SpecialQDiagram b)]
|
|
-> [Connection]
|
|
-> [(Name, SpecialQDiagram b)]
|
|
rotateNodes positionMap nameDiagramMap edges = map rotateDiagram nameDiagramMap
|
|
where
|
|
rotateDiagram (name, originalDia) = (name, nameDiagram name transformedDia)
|
|
where
|
|
transformedDia = if flippedDist < unflippedDist
|
|
then rotateBy flippedAngle . reflectX $ originalDia True flippedAngle
|
|
else rotateBy unflippedAngle $ originalDia False unflippedAngle
|
|
(unflippedAngle, unflippedDist) = minAngleForDia (originalDia False 0)
|
|
(flippedAngle, flippedDist) = minAngleForDia (reflectX $ originalDia True 0)
|
|
--minAngleForDia :: QDiagram b V2 Double m -> (Double, Double)
|
|
minAngleForDia dia = minAngle where
|
|
--ports = Debug.Trace.trace ((show $ names dia) ++ "\n") $ names dia
|
|
ports = names dia
|
|
namesOfPortsWithLines = connectedPorts edges name
|
|
|
|
iconInMap :: (Int, Name, Maybe Int) -> Bool
|
|
iconInMap (_, otherIconName, _) = Map.member otherIconName positionMap
|
|
|
|
getPortPoint :: Int -> P2 Double
|
|
getPortPoint x =
|
|
-- TODO remove partial function head.
|
|
head $ fromMaybeError
|
|
("rotateNodes: port not found. Port: " ++ show x ++ ". Valid ports: " ++ show ports)
|
|
(lookup (toName x) ports)
|
|
|
|
makePortEdge :: (Int, Name, Maybe Int) -> (P2 Double, P2 Double)
|
|
makePortEdge (portInt, otherIconName, _) =
|
|
(getPortPoint portInt, getFromMapAndScale positionMap otherIconName)
|
|
|
|
portEdges = map makePortEdge $ filter iconInMap namesOfPortsWithLines
|
|
|
|
minAngle = angleWithMinDist (getFromMapAndScale positionMap name) portEdges
|
|
|
|
|
|
type LayoutResult a b = Gr (GV.AttributeNode (Name, b)) (GV.AttributeNode a)
|
|
|
|
placeNodes :: Ord c =>
|
|
LayoutResult a c
|
|
-> [(Name, Bool -> Double -> SpecialQDiagram b)]
|
|
-> [Connection]
|
|
-> SpecialQDiagram b
|
|
placeNodes layoutResult nameDiagramMap edges = mconcat placedNodes
|
|
where
|
|
positionMap = Map.mapKeys fst $ fst $ getGraph layoutResult
|
|
rotatedNameDiagramMap = rotateNodes positionMap nameDiagramMap edges
|
|
placedNodes = map placeNode rotatedNameDiagramMap
|
|
--placedNodes = map placeNode nameDiagramMap
|
|
-- todo: Not sure if the diagrams should already be centered at this point.
|
|
placeNode (name, diagram) = place (centerXY diagram) (graphvizScaleFactor *^ (positionMap Map.! name))
|
|
|
|
customLayoutParams :: GV.GraphvizParams n v e () v
|
|
customLayoutParams = GV.defaultParams{
|
|
GV.globalAttributes = [
|
|
GV.NodeAttrs [GVA.Shape GVA.BoxShape]
|
|
--GV.NodeAttrs [GVA.Shape GVA.Circle]
|
|
, GV.GraphAttrs
|
|
[
|
|
--GVA.Overlap GVA.KeepOverlaps,
|
|
--GVA.Overlap GVA.ScaleOverlaps,
|
|
GVA.Overlap $ GVA.PrismOverlap (Just 5000),
|
|
GVA.Splines GVA.LineEdges,
|
|
GVA.OverlapScaling 8,
|
|
--GVA.OverlapScaling 4,
|
|
GVA.OverlapShrink True
|
|
]
|
|
],
|
|
GV.fmtEdge = const [GV.arrowTo GV.noArrow]
|
|
}
|
|
|
|
doGraphLayout :: forall b e.
|
|
SpecialBackend b =>
|
|
Gr (Name, Icon) e
|
|
-> [(Name, Bool -> Double -> SpecialQDiagram b)]
|
|
-> [Connection]
|
|
-> IO (SpecialQDiagram b)
|
|
doGraphLayout graph nameDiagramMap edges = do
|
|
layoutResult <- layoutGraph' layoutParams GVA.Neato graph
|
|
-- layoutResult <- layoutGraph' layoutParams GVA.Fdp graph
|
|
return $ placeNodes layoutResult nameDiagramMap edges
|
|
where
|
|
layoutParams :: GV.GraphvizParams Int (Name,Icon) e () (Name,Icon)
|
|
--layoutParams :: GV.GraphvizParams Int l el Int l
|
|
layoutParams = customLayoutParams{
|
|
GV.fmtNode = nodeAttribute
|
|
}
|
|
nodeAttribute :: (Int, (Name, Icon)) -> [GV.Attribute]
|
|
nodeAttribute (_, (_, nodeIcon)) =
|
|
-- GVA.Width and GVA.Height have a minimum of 0.01
|
|
--[GVA.Width diaWidth, GVA.Height diaHeight]
|
|
[GVA.Width circleDiameter, GVA.Height circleDiameter]
|
|
where
|
|
-- This type annotation (:: SpecialQDiagram b) requires Scoped Typed Variables, which only works if the function's
|
|
-- type signiture has "forall b e."
|
|
dia = iconToDiagram nodeIcon False 0 :: SpecialQDiagram b
|
|
|
|
diaWidth = drawingToGraphvizScaleFactor * width dia
|
|
diaHeight = drawingToGraphvizScaleFactor * height dia
|
|
circleDiameter' = max diaWidth diaHeight
|
|
circleDiameter = if circleDiameter' <= 0.01 then error ("circleDiameter too small: " ++ show circleDiameter') else circleDiameter'
|
|
|
|
-- | Given a Drawing, produce a Diagram complete with rotated/flipped icons and
|
|
-- lines connecting ports and icons. IO is needed for the GraphViz layout.
|
|
renderDrawing ::
|
|
SpecialBackend b =>
|
|
Drawing -> IO (SpecialQDiagram b)
|
|
renderDrawing = renderIconGraph . drawingToIconGraph
|
|
|
|
renderIngSyntaxGraph ::
|
|
SpecialBackend b =>
|
|
Gr (Name, SyntaxNode) Edge -> IO (SpecialQDiagram b)
|
|
renderIngSyntaxGraph = renderIconGraph . ING.nmap (Control.Arrow.second nodeToIcon)
|
|
|
|
renderIconGraph :: SpecialBackend b => Gr (Name, Icon) Edge -> IO (SpecialQDiagram b)
|
|
renderIconGraph iconGraph = diagramAction where
|
|
edges = ING.edgeLabel <$> ING.labEdges iconGraph
|
|
connections = fmap edgeConnection edges
|
|
diagramAction = makeConnections edges <$>
|
|
doGraphLayout iconGraph (makeNamedMapFromGraph iconGraph) connections
|