wumpus-core (empty) → 0.12.0
raw patch · 22 files changed
+5522/−0 lines, 22 filesdep +algebradep +basedep +containerssetup-changed
Dependencies added: algebra, base, containers, data-aviary, dlist, monadLib, old-time, vector-space, wl-pprint, xml
Files
- LICENSE +30/−0
- Setup.hs +4/−0
- demo/LabelPic.hs +145/−0
- demo/Picture.hs +133/−0
- src/Wumpus/Core.hs +67/−0
- src/Wumpus/Core/AffineTrans.hs +183/−0
- src/Wumpus/Core/BoundingBox.hs +215/−0
- src/Wumpus/Core/Colour.hs +223/−0
- src/Wumpus/Core/FontSize.hs +137/−0
- src/Wumpus/Core/Geometry.hs +627/−0
- src/Wumpus/Core/GraphicsState.hs +175/−0
- src/Wumpus/Core/OutputPostScript.hs +373/−0
- src/Wumpus/Core/OutputSVG.hs +284/−0
- src/Wumpus/Core/Picture.hs +443/−0
- src/Wumpus/Core/PictureInternal.hs +461/−0
- src/Wumpus/Core/PictureLanguage.hs +317/−0
- src/Wumpus/Core/PostScript.hs +525/−0
- src/Wumpus/Core/SVG.hs +434/−0
- src/Wumpus/Core/TextEncoding.hs +119/−0
- src/Wumpus/Core/TextLatin1.hs +250/−0
- src/Wumpus/Core/Utils.hs +270/−0
- wumpus-core.cabal +107/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2008 Stephen Peter Tetley++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of his contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,4 @@+#!/usr/bin/env runhaskell++import Distribution.Simple+main = defaultMain
+ demo/LabelPic.hs view
@@ -0,0 +1,145 @@+{-# OPTIONS -Wall #-}++module LabelPic where++import Wumpus.Core+++--------------------------------------------------------------------------------++++drawBounds :: (Fractional u, Ord u) => Picture u -> Picture u+drawBounds p = p `over` (frame $ cstroke () ph) where+ ph = vertexPath $ corners $ boundary p++--------------------------------------------------------------------------------+++peru :: PSRgb+peru = RGB3 0.804 0.522 0.247++plum :: PSRgb+plum = RGB3 0.867 0.627 0.867++black :: PSRgb+black = RGB3 0 0 0 ++++lbl1 :: Picture Double+lbl1 = line1 -//- line2 where+ line1 = frame (textlabel attrs zeroPt "Hello")+ line2 = frame (textlabel attrs zeroPt "World")+ attrs = (peru, FontAttr "Helvetica" "Helvetica" SVG_REGULAR 12) +++demo01 :: IO ()+demo01 = do + writeEPS_latin1 "./out/label01.eps" lbl1+ writeSVG_latin1 "./out/label01.svg" lbl1++demo02 :: IO ()+demo02 = do + writeEPS_latin1 "./out/label02.eps" p1+ writeSVG_latin1 "./out/label02.svg" p1+ where+ p1 = lbl1 ->- lbl1 ->- (rotateAbout (pi/4) (center lbl1) lbl1) ->- lbl1++demo03 :: IO ()+demo03 = do + writeEPS_latin1 "./out/label03.eps" p1+ writeSVG_latin1 "./out/label03.svg" p1+ where+ p1 = (drawBounds lbl1) ->- + (drawBounds lbl1) ->- + (drawBounds $ rotateAbout (pi/4) (center lbl1) lbl1) ->- + (drawBounds lbl1)++++demo04 :: IO ()+demo04 = do+ writeEPS_latin1 "./out/label04.eps" p1+ writeSVG_latin1 "./out/label04.svg" p1+ where+ p1 = (drawBounds lbl1) + `over` (drawBounds $ scale 2 2 lbl1)+ `over` (drawBounds $ scale 3 3 lbl1)+++++bigA, bigB, bigT :: Picture Double+bigA = bigLetter black 'A'+bigB = bigLetter peru 'B'+bigT = bigLetter plum 'T'++bigLetter :: PSRgb -> Char -> Picture Double+bigLetter col ch = uniformScale 5 $ frame $ textlabel attrs zeroPt [ch]+ where+ attrs = (col, FontAttr "Helvetica" "Helvetica" SVG_REGULAR 12) +++-- | A should be above B, above T+demo05 :: IO ()+demo05 = do + writeEPS_latin1 "./out/label05.eps" p1+ writeSVG_latin1 "./out/label05.svg" p1+ where+ p1 = uniformScale 10 $ stackOntoCenter [bigA, bigB] bigT+++demo06 :: IO ()+demo06 = do + writeEPS_latin1 "./out/label06.eps" p1+ writeSVG_latin1 "./out/label06.svg" p1+ where+ p1 = hsep 20 (fn 'a') (map fn "abcdefg")+ fn = drawBounds . bigLetter peru+++demo07 :: IO ()+demo07 = do + writeEPS_latin1 "./out/label07.eps" p1+ writeSVG_latin1 "./out/label07.svg" p1+ where+ p1 = pA ->- pB ->- pC ->- pA+ + pA = drawBounds bigA+ pB = drawBounds $ uniformScale 2 bigB+ pC = drawBounds $ move 0 10 $ bigLetter peru 'C'+++demo08 :: IO ()+demo08 = do + writeEPS_latin1 "./out/label08.eps" p1+ writeSVG_latin1 "./out/label08.svg" p1+ where+ p1 = hcat pA [pA, pB, pC]+ + pA = drawBounds bigA+ pB = drawBounds $ uniformScale 2 bigB+ pC = drawBounds $ move 0 10 $ bigLetter peru 'C'++demo09 :: IO ()+demo09 = do + writeEPS_latin1 "./out/label09.eps" p1+ writeSVG_latin1 "./out/label09.svg" p1+ where+ p1 = (bigA -//- bigB) ->- (bigA -\\- bigB) + +demo10 :: IO ()+demo10 = do + writeEPS_latin1 "./out/label10.eps" p1+ writeSVG_latin1 "./out/label10.svg" p1+ where+ p1 :: Picture Double+ p1 = frame $ textlabel () zeroPt "myst&#egrave;re"+++main :: IO ()+main = sequence_+ [ demo01, demo02, demo03, demo04, demo05+ , demo06, demo07, demo08, demo09, demo10+ ]
+ demo/Picture.hs view
@@ -0,0 +1,133 @@+{-# OPTIONS -Wall #-}++module Picture where++import Wumpus.Core++++peru :: PSRgb+peru = RGB3 0.804 0.522 0.247++plum :: PSRgb+plum = RGB3 0.867 0.627 0.867++black :: PSRgb+black = RGB3 0 0 0 +++square :: DPicture +square = frame $ cstroke () $ vertexPath+ [ P2 0 0, P2 40 0, P2 40 40, P2 0 40 ]++funnyshape :: DPicture+funnyshape = frame $ cstroke () $ vertexPath+ [ P2 0 0, P2 20 0, P2 20 10, P2 30 10, P2 30 20, P2 0 20 ]+++demo01 :: IO ()+demo01 = do + writePS_latin1 "./out/picture01.ps" [funnyshape ->- square]+ writeSVG_latin1 "./out/picture01.svg" $ funnyshape ->- square+++pic1 :: Picture Double+pic1 = square ->- (funnyshape ->- funnyshape) ->- square++squares :: Picture Double+squares = square ->- square ->- square++demo02 :: IO ()+demo02 = do + writePS_latin1 "./out/picture02.ps" [squares]+ writeSVG_latin1 "./out/picture02.svg" squares + ++demo03 :: IO ()+demo03 = do + writeEPS_latin1 "./out/picture03.eps" p1 + writeSVG_latin1 "./out/picture03.svg" p1+ where + p1 = square ->- (rotate45About (center squares) squares) ->- square+++demo04 :: IO ()+demo04 = do + writeEPS_latin1 "./out/picture04.eps" p1+ writeSVG_latin1 "./out/picture04.svg" p1+ where+ p1 = square -//- squares+ ++demo05 :: IO ()+demo05 = do + writeEPS_latin1 "./out/picture05.eps" p1+ writeSVG_latin1 "./out/picture05.svg" p1+ where+ p1 = square `over` (rotate (pi/4) squares)+ ++demo06 :: IO ()+demo06 = do + writeEPS_latin1 "./out/picture06.eps" p1+ writeSVG_latin1 "./out/picture06.svg" p1+ where+ p1 = square `over` (rotate45 square)+++-- Note the move via @at@ is not apparent when SVG file is +-- viewed with Mozilla or Chrome - check picture7a.svg+-- We only see that the move has /worked/ when we compose+-- with with `over` a square at the origin. ++demo07 :: IO ()+demo07 = do + writeEPS_latin1 "./out/picture07.eps" p1+ writeSVG_latin1 "./out/picture07.svg" p1+ writeSVG_latin1 "./out/picture07a.svg" p2+ where+ p1 = square `over` p2+ p2 = (square `at` (P2 100 30)) -@- (rotate45 square)+++demo08 :: IO ()+demo08 = do + writeEPS_latin1 "./out/picture08.eps" p1+ writeSVG_latin1 "./out/picture08.svg" p1+ where+ p1 = hspace 20 square square++mkFilledSquare :: (PSColour c, Fill c) => c -> DPicture +mkFilledSquare col = frame $ fill col $ vertexPath+ [ P2 0 0, P2 40 0, P2 40 40, P2 0 40 ]+++demo09 :: IO ()+demo09 = do + writeEPS_latin1 "./out/picture09.eps" p1+ writeSVG_latin1 "./out/picture09.svg" p1+ where+ p1 = (alignH HTop s1 s2) `op` s3+ s1 = uniformScale 1.5 $ mkFilledSquare plum + s2 = uniformScale 1.75 $ mkFilledSquare peru+ s3 = scale 3 1.5 $ mkFilledSquare black+ op = alignH HBottom+ ++demo10 :: IO ()+demo10 = do + writeEPS_latin1 "./out/picture10.eps" p1+ writeSVG_latin1 "./out/picture10.svg" p1+ where+ p1 = vsepA VRight 5 s1 [s2,s3]+ s1 = uniformScale 1.5 $ mkFilledSquare plum + s2 = uniformScale 1.75 $ mkFilledSquare peru+ s3 = scale 3 1.5 $ mkFilledSquare black+ +++main :: IO ()+main = sequence_+ [ demo01, demo02, demo03, demo04, demo05+ , demo06, demo07, demo08, demo09, demo10+ ]
+ src/Wumpus/Core.hs view
@@ -0,0 +1,67 @@+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD3+--+-- Maintainer : Stephen Tetley <stephen.tetley@gmail.com>+-- Stability : highly unstable+-- Portability : GHC+--+-- Common interface to Wumpus.Core...+-- +-- +--------------------------------------------------------------------------------+++module Wumpus.Core+ (+ module Wumpus.Core.AffineTrans + , module Wumpus.Core.BoundingBox+ , module Wumpus.Core.Colour+ , module Wumpus.Core.FontSize+ , module Wumpus.Core.Geometry+ , module Wumpus.Core.GraphicsState+ , module Wumpus.Core.OutputPostScript+ , module Wumpus.Core.OutputSVG+ , module Wumpus.Core.Picture+ , module Wumpus.Core.PictureLanguage+ , module Wumpus.Core.TextEncoding++ -- Export from Picture Internal+ , Picture+ , DPicture+ , Primitive+ , DPrimitive+ , Path+ , DPath+ , PathSegment+ , DPathSegment+ , Label+ , DLabel++ , PathProps -- Better hidden?+ , LabelProps -- "+ , EllipseProps -- "+ , DrawPath -- "+ , DrawEllipse -- "++ ) where++import Wumpus.Core.AffineTrans+import Wumpus.Core.BoundingBox+import Wumpus.Core.Colour hiding ( black, white, red, green, blue )+import Wumpus.Core.FontSize+import Wumpus.Core.Geometry+import Wumpus.Core.GraphicsState+import Wumpus.Core.OutputPostScript+import Wumpus.Core.OutputSVG+import Wumpus.Core.Picture+import Wumpus.Core.PictureInternal+import Wumpus.Core.PictureLanguage+import Wumpus.Core.TextEncoding+++
+ src/Wumpus/Core/AffineTrans.hs view
@@ -0,0 +1,183 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleContexts #-}+{-# OPTIONS -Wall #-}++------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.AffineTrans+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Affine transformations+--------------------------------------------------------------------------------++module Wumpus.Core.AffineTrans+ ( + -- * Type classes+ Rotate(..)+ , RotateAbout(..)+ , Scale(..)+ , Translate(..)++ -- * Common rotations+ , rotate30+ , rotate30About+ , rotate45+ , rotate45About+ , rotate60+ , rotate60About+ , rotate90+ , rotate90About+ , rotate120+ , rotate120About+ + -- * Common scalings+ , uniformScale+ , reflectX+ , reflectY++ -- * Translate by a vector+ , translateBy+ + -- * Reflections in supplied plane rather than about the origin+ , reflectXPlane+ , reflectYPlane ++ ) where++import Wumpus.Core.Geometry++++--------------------------------------------------------------------------------+-- Affine transformations +++-- Rotate++class Rotate t where+ rotate :: Radian -> t -> t+++instance (Floating a, Real a) => Rotate (Point2 a) where+ rotate a = ((rotationMatrix a) *#)++instance (Floating a, Real a) => Rotate (Vec2 a) where+ rotate a = ((rotationMatrix a) *#)++-- Rotate about++class RotateAbout t where+ rotateAbout :: Radian -> Point2 (DUnit t) -> t -> t +++instance (Floating a, Real a) => RotateAbout (Point2 a) where+ rotateAbout a pt = ((originatedRotationMatrix a pt) *#) +++instance (Floating a, Real a) => RotateAbout (Vec2 a) where+ rotateAbout a pt = ((originatedRotationMatrix a pt) *#) + +--------------------------------------------------------------------------------+-- Scale++class Scale t where+ scale :: DUnit t -> DUnit t -> t -> t++instance Num u => Scale (Point2 u) where+ scale x y = ((scalingMatrix x y) *#) ++instance Num u => Scale (Vec2 u) where+ scale x y = ((scalingMatrix x y) *#) ++--------------------------------------------------------------------------------+-- Translate++class Translate t where+ translate :: DUnit t -> DUnit t -> t -> t++-- | translate @x@ @y@.+instance Num u => Translate (Point2 u) where+ translate x y = ((translationMatrix x y) *#)++instance Num u => Translate (Vec2 u) where+ translate x y = ((translationMatrix x y) *#)+++-------------------------------------------------------------------------------- +-- Common rotations+++++rotate30 :: Rotate t => t -> t +rotate30 = rotate (pi/6) ++rotate30About :: (RotateAbout t, DUnit t ~ u) => Point2 u -> t -> t +rotate30About = rotateAbout (pi/6)+++rotate45 :: Rotate t => t -> t +rotate45 = rotate (pi/4) ++rotate45About :: (RotateAbout t, DUnit t ~ u) => Point2 u -> t -> t +rotate45About = rotateAbout (pi/4)+++rotate60 :: Rotate t => t -> t +rotate60 = rotate (2*pi/3) ++rotate60About :: (RotateAbout t, DUnit t ~ u) => Point2 u -> t -> t +rotate60About = rotateAbout (2*pi/3)++rotate90 :: Rotate t => t -> t +rotate90 = rotate (pi/2) ++rotate90About :: (RotateAbout t, DUnit t ~ u) => Point2 u -> t -> t +rotate90About = rotateAbout (pi/2)+++rotate120 :: Rotate t => t -> t +rotate120 = rotate (4*pi/3) ++rotate120About :: (RotateAbout t, DUnit t ~ u) => Point2 u -> t -> t +rotate120About = rotateAbout (4*pi/3)++++--------------------------------------------------------------------------------+-- Common scalings++uniformScale :: (Scale t, DUnit t ~ u) => u -> t -> t +uniformScale a = scale a a +++reflectX :: (Num u, Scale t, DUnit t ~ u) => t -> t+reflectX = scale (-1) 1++reflectY :: (Num u, Scale t, DUnit t ~ u) => t -> t+reflectY = scale 1 (-1)++--------------------------------------------------------------------------------+-- translations++translateBy :: (Translate t, DUnit t ~ u) => Vec2 u -> t -> t +translateBy (V2 x y) = translate x y+++--------------------------------------------------------------------------------+-- Translation and scaling++-- | Reflect in the X plane that intersects the supplied point. +reflectXPlane :: (Num u, Scale t, Translate t, u ~ DUnit t) + => Point2 u -> t -> t+reflectXPlane (P2 x y) = translate x y . scale (-1) 1 . translate (-x) (-y)++-- | Reflect in the Y plane that intersects the supplied point.+reflectYPlane :: (Num u, Scale t, Translate t, u ~ DUnit t) + => Point2 u -> t -> t+reflectYPlane (P2 x y) = translate x y . scale 1 (-1) . translate (-x) (-y)
+ src/Wumpus/Core/BoundingBox.hs view
@@ -0,0 +1,215 @@+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.BoundingBox+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Bounding box with no notion of \'empty\'.+--+--------------------------------------------------------------------------------++module Wumpus.Core.BoundingBox + ( + -- * Types+ BoundingBox(..)+ , DBoundingBox+ , CardinalPoint(..)++ -- * Type class+ , Boundary(..)+ + -- * Operations+ , bbox+ , obbox+ , union + , trace+ , corners+ , lowerLeftUpperRight+ , withinBB+ , boundaryWidth+ , boundaryHeight+ , boundaryBottomLeft+ , boundaryTopRight+ , boundaryTopLeft+ , boundaryBottomRight+ , boundaryPoint+ , leftPlane+ , rightPlane+ , lowerPlane+ , upperPlane++ ) where++import Wumpus.Core.AffineTrans+import Wumpus.Core.Geometry+import Wumpus.Core.Utils ( CMinMax(..), within )++import Data.Semigroup++import Text.PrettyPrint.Leijen hiding ( width )++++-- | Bounding box of a picture.+-- +-- We cannot construct empty pictures - so bounding boxes too a +-- saved the obligation to be empty.+-- +data BoundingBox a = BBox { + ll_corner :: Point2 a, + ur_corner :: Point2 a + }+ deriving (Eq,Show)++type DBoundingBox = BoundingBox Double++data CardinalPoint = C | N | NE | E | SE | S | SW | W | NW+ deriving (Eq,Show)+++--------------------------------------------------------------------------------+-- instances++-- BBox is NOT monoidal - it\'s much simpler that way.++instance Ord a => Semigroup (BoundingBox a) where+ append = union+++instance Pretty a => Pretty (BoundingBox a) where+ pretty (BBox p0 p1) = text "|_" <+> pretty p0 <+> pretty p1 <+> text "_|" +++--------------------------------------------------------------------------------+-- ++type instance DUnit (BoundingBox u) = u++instance (Num u, Ord u) => Scale (BoundingBox u) where+ scale x y bb = trace $ map (scale x y) $ corners bb++++--------------------------------------------------------------------------------+-- Boundary class++class Boundary a where+ boundary :: a -> BoundingBox (DUnit a)+++--------------------------------------------------------------------------------+++instance Pointwise (BoundingBox a) where+ type Pt (BoundingBox a) = Point2 a+ pointwise f (BBox bl tr) = BBox (f bl) (f tr)+++--------------------------------------------------------------------------------++bbox :: Point2 a -> Point2 a -> BoundingBox a+bbox = BBox +++-- | Create a BoundingBox with bottom left corner at the origin,+-- and dimensions @w@ and @h@.+obbox :: Num a => a -> a -> BoundingBox a+obbox w h = BBox zeroPt (P2 w h)+++union :: Ord a => BoundingBox a -> BoundingBox a -> BoundingBox a+BBox ll ur `union` BBox ll' ur' = BBox (cmin ll ll') (cmax ur ur')++-- Trace the point list finding the /extremity/...++trace :: (Num a, Ord a) => [Point2 a] -> BoundingBox a+trace (p:ps) = uncurry BBox $ foldr (\z (a,b) -> (cmin z a, cmax z b) ) (p,p) ps+trace [] = error $ "BoundingBox.trace called in empty list"+++corners :: BoundingBox a -> [Point2 a]+corners (BBox bl@(P2 x0 y0) tr@(P2 x1 y1)) = [bl, br, tr, tl] where+ br = P2 x1 y0+ tl = P2 x0 y1+++lowerLeftUpperRight :: (a,a,a,a) -> BoundingBox a -> (a,a,a,a)+lowerLeftUpperRight _ (BBox (P2 x0 y0) (P2 x1 y1)) = (x0,y0,x1,y1)++++withinBB :: Ord a => Point2 a -> BoundingBox a -> Bool+withinBB p (BBox ll ur) = within p ll ur+++boundaryWidth :: Num a => BoundingBox a -> a+boundaryWidth (BBox (P2 xmin _) (P2 xmax _)) = xmax - xmin++boundaryHeight :: Num a => BoundingBox a -> a+boundaryHeight (BBox (P2 _ ymin) (P2 _ ymax)) = ymax - ymin+++--------------------------------------------------------------------------------++-- Points on the boundary+++boundaryBottomLeft :: BoundingBox a -> Point2 a+boundaryBottomLeft (BBox p0 _ ) = p0++boundaryTopRight :: BoundingBox a -> Point2 a+boundaryTopRight (BBox _ p1) = p1++boundaryTopLeft :: BoundingBox a -> Point2 a+boundaryTopLeft (BBox (P2 x _) (P2 _ y)) = P2 x y++boundaryBottomRight :: BoundingBox a -> Point2 a+boundaryBottomRight (BBox (P2 _ y) (P2 x _)) = P2 x y+++boundaryPoint :: Fractional a + => CardinalPoint -> BoundingBox a -> Point2 a+boundaryPoint loc (BBox (P2 x0 y0) (P2 x1 y1)) = fn loc where+ fn C = P2 xMid yMid+ fn N = P2 xMid y1+ fn NE = P2 x1 y1+ fn E = P2 x1 yMid+ fn SE = P2 x1 y0+ fn S = P2 xMid y0+ fn SW = P2 x0 y0+ fn W = P2 x0 yMid+ fn NW = P2 x0 y1 ++ xMid = x0 + 0.5 * (x1 - x0)+ yMid = y0 + 0.5 * (y1 - y0)+++--------------------------------------------------------------------------------++-- /planes/ on the bounding box++-- Are these really worthwhile ? ...++leftPlane :: BoundingBox a -> a+leftPlane (BBox (P2 l _) _) = l++rightPlane :: BoundingBox a -> a+rightPlane (BBox _ (P2 r _)) = r++lowerPlane :: BoundingBox a -> a+lowerPlane (BBox (P2 _ l) _) = l++upperPlane :: BoundingBox a -> a+upperPlane (BBox _ (P2 _ u)) = u++++++
+ src/Wumpus/Core/Colour.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.Colour+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD3+--+-- Maintainer : Stephen Tetley <stephen.tetley@gmail.com>+-- Stability : highly unstable+-- Portability : GHC+--+-- RGB, HSB, Gray colour types.+--+--------------------------------------------------------------------------------+++module Wumpus.Core.Colour + (+ -- * Colour types+ RGB3(..)+ , DRGB+ , HSB3(..)+ , DHSB+ , Gray(..)+ , DGray++ -- * Operations+ , rgb2hsb+ , hsb2rgb++ , rgb2gray+ , gray2rgb++ , hsb2gray+ , gray2hsb+ + -- * Predefined colours+ , black+ , white+ , red+ , green+ , blue++ ) where++import Wumpus.Core.Utils+++import Data.VectorSpace++-- | Red-Green-Blue - no alpha.+data RGB3 a = RGB3 !a !a !a+ deriving (Eq,Show)++-- | RGB representated by Double - values should be in the range+-- 0.0 to 1.0. +-- +-- 1.0 represents full saturation, for instance red is +-- 1.0, 0.0, 0.0.+type DRGB = RGB3 Double+++-- | Hue-Saturation-Brightness.+data HSB3 a = HSB3 !a !a !a + deriving (Eq,Show)++-- | HSB represented by Double - values should be in the range+-- 0.0 to 1.0.+type DHSB = HSB3 Double +++newtype Gray a = Gray a+ deriving (Eq,Num,Ord,Show)++-- | Gray represented by a Double - values should be in the range+-- 0.0 (black) to 1.0 (white).+type DGray = Gray Double+++instance Num a => Num (RGB3 a) where+ (+) (RGB3 a b c) (RGB3 x y z) = RGB3 (a+x) (b+y) (c+z)+ (-) (RGB3 a b c) (RGB3 x y z) = RGB3 (a-x) (b-y) (c-z)+ (*) (RGB3 a b c) (RGB3 x y z) = RGB3 (a*x) (b*y) (c*z)+ abs (RGB3 a b c) = RGB3 (abs a) (abs b) (abs c)+ negate (RGB3 a b c) = RGB3 (negate a) (negate b) (negate c)+ signum (RGB3 a b c) = RGB3 (signum a) (signum b) (signum c)+ fromInteger i = RGB3 (fromInteger i) (fromInteger i) (fromInteger i)++instance Fractional a => Fractional (RGB3 a) where+ (/) (RGB3 a b c) (RGB3 x y z) = RGB3 (a/x) (b/y) (c/z)+ recip (RGB3 a b c) = RGB3 (recip a) (recip b) (recip c)+ fromRational a = RGB3 (fromRational a) (fromRational a) (fromRational a)++ +instance Num a => AdditiveGroup (RGB3 a) where+ zeroV = RGB3 0 0 0+ (^+^) = (+)+ negateV = negate+++instance (Num a, VectorSpace a) => VectorSpace (RGB3 a) where+ type Scalar (RGB3 a) = Scalar a+ s *^ (RGB3 a b c) = RGB3 (s*^a) (s*^b) (s*^c)++++instance Num a => Num (HSB3 a) where+ (+) (HSB3 a b c) (HSB3 x y z) = HSB3 (a+x) (b+y) (c+z)+ (-) (HSB3 a b c) (HSB3 x y z) = HSB3 (a-x) (b-y) (c-z)+ (*) (HSB3 a b c) (HSB3 x y z) = HSB3 (a*x) (b*y) (c*z)+ abs (HSB3 a b c) = HSB3 (abs a) (abs b) (abs c)+ negate (HSB3 a b c) = HSB3 (negate a) (negate b) (negate c)+ signum (HSB3 a b c) = HSB3 (signum a) (signum b) (signum c)+ fromInteger i = HSB3 (fromInteger i) (fromInteger i) (fromInteger i)++instance Fractional a => Fractional (HSB3 a) where+ (/) (HSB3 a b c) (HSB3 x y z) = HSB3 (a/x) (b/y) (c/z)+ recip (HSB3 a b c) = HSB3 (recip a) (recip b) (recip c)+ fromRational a = HSB3 (fromRational a) (fromRational a) (fromRational a)+++ +instance Num a => AdditiveGroup (HSB3 a) where+ zeroV = HSB3 0 0 0+ (^+^) = (+)+ negateV = negate++++instance (Num a, VectorSpace a) => VectorSpace (HSB3 a) where+ type Scalar (HSB3 a) = Scalar a+ s *^ (HSB3 a b c) = HSB3 (s*^a) (s*^b) (s*^c)++--------------------------------------------------------------------------------+-- Operations+++vE :: DRGB+vE = RGB3 1 1 1++-- Acknowledgment - the conversion functions are derived from+-- the documentation to Dr. Uwe Kern's xcolor LaTeX package++++rgb2hsb :: DRGB -> DHSB+rgb2hsb (RGB3 r g b) = HSB3 hue sat bri+ where+ x = max3 r g b+ y = med3 r g b+ z = min3 r g b++ bri = x++ (sat,hue) = if x==z then (0,0) else ((x-z)/x, f $ (x-y)/(x-z))+ + f n | r >= g && g >= b = (1/6) * (1-n) + | g >= r && r >= b = (1/6) * (1+n)+ | g >= b && b >= r = (1/6) * (3-n)+ | b >= g && g >= r = (1/6) * (3+n)+ | b >= r && r >= g = (1/6) * (5-n)+ | otherwise = (1/6) * (5+n)++++hsb2rgb :: DHSB -> DRGB+hsb2rgb (HSB3 hue sat bri) = bri *^ (vE - (sat *^ fV))+ where+ i :: Int+ i = floor $ (6 * hue)+ f = (6 * hue) - fromIntegral i+ fV | i == 0 = RGB3 0 (1-f) 1 + | i == 1 = RGB3 f 0 1+ | i == 2 = RGB3 1 0 (1-f)+ | i == 3 = RGB3 1 f 0+ | i == 4 = RGB3 (1-f) 1 0+ | i == 5 = RGB3 0 1 f+ | otherwise = RGB3 0 1 1+ +rgb2gray :: DRGB -> DGray+rgb2gray (RGB3 r g b) = Gray $ 0.3 * r + 0.59 * g + 0.11 * b ++gray2rgb :: DGray -> DRGB+gray2rgb (Gray a) = a *^ vE++hsb2gray :: DHSB -> DGray+hsb2gray (HSB3 _ _ b) = Gray b ++gray2hsb :: DGray -> DHSB+gray2hsb (Gray a) = HSB3 0 0 a+++++--------------------------------------------------------------------------------++-- Some colours++-- There will be name clashes with the X11Colours / SVGColours.++-- | Black - 0.0, 0.0, 0.0.+black :: DRGB+black = RGB3 0 0 0++-- | White - 1.0, 1.0, 1.0.+white :: DRGB+white = RGB3 1 1 1++-- | Red - 1.0, 0.0, 0.0.+red :: DRGB+red = RGB3 1 0 0++-- | Green - 0.0, 1.0, 0.0.+green :: DRGB +green = RGB3 0 1 0++-- | Blue - 0.0, 0.0, 1.0.+blue :: DRGB+blue = RGB3 0 0 1+
+ src/Wumpus/Core/FontSize.hs view
@@ -0,0 +1,137 @@+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.FontSize+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Text handling+-- +--------------------------------------------------------------------------------++module Wumpus.Core.FontSize+ ( + + -- * Type synonym+ FontSize++ -- * Courier metrics at 48 point+ , courier48_width+ , courier48_body_height+ , courier48_height+ , courier48_descender_depth+ , courier48_spacer_width++ , widthAt48pt+ , textWidth+ , textHeight+ , descenderDepth++ , textBounds++ ) where++import Wumpus.Core.BoundingBox+import Wumpus.Core.Geometry++import Data.AffineSpace -- vector-space++type CharCount = Int+type FontSize = Int++-- | The width of a letter in Courier at 48 pt.+--+-- The value is not entirely accurate but it is satisfactory.+courier48_width :: Num u => u+courier48_width = 26+++-- | The height of a letter without accents, ascenders or +-- descenders in Courier at 48 pt .+--+-- The value is not entirely accurate but it is satisfactory - +-- some letters are taller than others (e.g. numbers are taller +-- then capitals).+courier48_body_height :: Num u => u +courier48_body_height = 30+++-- | The /common maximum/ height of a letter in Courier at 48pt.+--+-- By common maximum the letter is allowed to have both an accent +-- or ascender and a descender.+--+-- Naturally the height is 48.0.+--+courier48_height :: Num u => u+courier48_height = 48+++-- | The depth of a descender in Courier at 48 pt.+-- +-- Also the height of an ascender.+courier48_descender_depth :: Num u => u +courier48_descender_depth = 9++++-- | The spacing between letters printed directly with +-- PostScript\'s show command for Courier at 48 pt.+--+-- The value is not entirely accurate but it is satisfactory.+courier48_spacer_width :: Num u => u+courier48_spacer_width = 3+++-- | Width of the supplied string when printed at 48pt.+widthAt48pt :: Fractional u => CharCount -> u+widthAt48pt n = courier48_width * len + courier48_spacer_width * len_sub+ where+ len = fromIntegral n+ len_sub = len - 1.0++--- | Text width at @sz@ point size of the string @s@. All+-- characters are counted literally - special chars may cause+-- problems (this a current deficiency of Wumpus).+textWidth :: Fractional u => FontSize -> CharCount -> u+textWidth sz n = (fromIntegral sz)/48 * widthAt48pt n++-- | Text height is just identity/double-coercion, i.e. +-- @18 == 18.0@. The /size/ of a font is (apparently) the maximum+-- height (body + descender max + ascender max).+textHeight :: Num u => FontSize -> u+textHeight = fromIntegral++-- | Descender depth for font size @sz@.+-- +-- (The metrics are taken from Courier, of course).+--+descenderDepth :: Fractional u => FontSize -> u+descenderDepth sz = (fromIntegral sz) / 48 * courier48_descender_depth++-- | Find the bounding box for the character count at the +-- supplied font-size.+-- +-- The supplied point represents the bottom left corner of the +-- a regular upper-case letter (that is without descenders).+-- The bounding box will always be /dropped/ to accommodate +-- ascenders - no interpretation of the string takes place to +-- see if it actually contains ascenders or descenders.+-- +-- The metrics used are derived from Courier - a monospaced font.+-- For variable width fonts the calculated bounding box will +-- usually be too long.+--+textBounds :: Fractional u + => FontSize -> Point2 u -> CharCount -> BoundingBox u+textBounds sz body_bl n = bbox bl tr where+ h = textHeight sz+ w = textWidth sz n+ dd = descenderDepth sz+ bl = body_bl .-^ V2 0 dd + tr = bl .+^ V2 w h+
+ src/Wumpus/Core/Geometry.hs view
@@ -0,0 +1,627 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS -Wall #-}++------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.Geometry+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- 2D geometry+--------------------------------------------------------------------------------++module Wumpus.Core.Geometry + ( + -- * Type family + DUnit+ + -- * Data types+ , Vec2(..)+ , DVec2+ , Point2(..)+ , DPoint2+ , Frame2(..)+ , DFrame2+ , Matrix3'3(..)+ , DMatrix3'3+ , Radian+++ -- * Pointwise type class+ , Pointwise(..)++ -- * Matrix multiply type class+ , MatrixMult(..)++ -- * Vector operations+ , hvec+ , vvec+ , avec++ -- * Point operations+ , zeroPt+ , langle++ -- * Frame operations+ , ortho+ , displaceOrigin+ , pointInFrame+ , frame2Matrix+ , matrix2Frame+ , frameProduct+ , standardFrame++ -- * Matrix contruction+ , identityMatrix+ , scalingMatrix+ , translationMatrix+ , rotationMatrix+ , originatedRotationMatrix++ -- * matrix operations+ , invert+ , determinant+ , transpose++ -- * Radian operations+ , req+ , toRadian+ , fromRadian+ , d2r+ , r2d++ ) where++import Wumpus.Core.Utils ( CMinMax(..), PSUnit(..) )++import Data.Aviary++import Data.AffineSpace+import Data.VectorSpace++import Text.PrettyPrint.Leijen hiding ( langle )++import Data.Function ( on )+import Data.Monoid+++--------------------------------------------------------------------------------++-- | Some unit of dimension usually double.++type family DUnit a :: *++++-- Datatypes ++-- Vectors++data Vec2 a = V2 !a !a+ deriving (Eq,Show)++type DVec2 = Vec2 Double++-- Points++data Point2 a = P2 !a !a+ deriving (Eq,Show)++type DPoint2 = Point2 Double++++-- | A two dimensional frame.+-- +-- The components are the two basis vectors @e0@ and @e1@ and +-- the origin @o@.+--+-- Typically these names for the elements will be used:+--+-- > Frame2 (V2 e0x e0y) (V2 e1x e1y) (P2 ox oy)+-- ++data Frame2 a = Frame2 (Vec2 a) (Vec2 a) (Point2 a)+ deriving (Eq,Show)++type DFrame2 = Frame2 Double++++-- | 3x3 matrix, considered to be in row-major form.+-- +-- > (M3'3 a b c+-- > d e f+-- > g h i)+--+-- For instance the rotation matrix is represented as+--+-- > ( cos(a) -sin(a) 0+-- > sin(a) cos(a) 0 +-- > 0 0 1 )+--+-- This is congruent with the form presented in Santos - +-- Example 45, page 17 extended to 3x3. +--+-- ref. David A. Santos /Multivariable and Vector Calculus/,+-- July 17, 2008 Version.+--+-- The right-most column is considered to represent a+-- coordinate:+--+-- > ( 1 0 x+-- > 0 1 y +-- > 0 0 1 ) +-- >+-- +-- So a translation matrix representing the displacement in x +-- of 40 and in y of 10 would be:+--+-- > ( 1 0 40+-- > 0 1 10 +-- > 0 0 1 ) +-- >+-- +++data Matrix3'3 a = M3'3 !a !a !a !a !a !a !a !a !a+ deriving (Eq)++type DMatrix3'3 = Matrix3'3 Double++++-- | Radian is represented with a distinct type. +-- Equality and ordering are approximate where the epsilon is 0.0001.+newtype Radian = Radian { getRadian :: Double }+ deriving (Num,Real,Fractional,Floating,RealFrac,RealFloat)+++--------------------------------------------------------------------------------+-- Family instances++type instance DUnit (Point2 a) = a+type instance DUnit (Vec2 a) = a+type instance DUnit (Frame2 a) = a+type instance DUnit (Matrix3'3 a) = a++--------------------------------------------------------------------------------+-- lifters / convertors++lift2Vec2 :: (a -> a -> a) -> Vec2 a -> Vec2 a -> Vec2 a+lift2Vec2 op (V2 x y) (V2 x' y') = V2 (x `op` x') (y `op` y')+++lift2Matrix3'3 :: (a -> a -> a) -> Matrix3'3 a -> Matrix3'3 a -> Matrix3'3 a+lift2Matrix3'3 op (M3'3 a b c d e f g h i) (M3'3 m n o p q r s t u) = + M3'3 (a `op` m) (b `op` n) (c `op` o) + (d `op` p) (e `op` q) (f `op` r) + (g `op` s) (h `op` t) (i `op` u)++++--------------------------------------------------------------------------------+-- instances+++instance Functor Vec2 where+ fmap f (V2 a b) = V2 (f a) (f b)+++instance Functor Point2 where+ fmap f (P2 a b) = P2 (f a) (f b)++instance Functor Matrix3'3 where+ fmap f (M3'3 m n o p q r s t u) = + M3'3 (f m) (f n) (f o) (f p) (f q) (f r) (f s) (f t) (f u)+++-- Vectors have a sensible Monoid instance as addition, points don't++++instance Num a => Monoid (Vec2 a) where+ mempty = V2 0 0+ mappend = lift2Vec2 (+) +++-- Affine frames also have a sensible Monoid instance++instance (Num a, InnerSpace (Vec2 a)) => Monoid (Frame2 a) where+ mempty = ortho zeroPt+ mappend = frameProduct++++++instance Show a => Show (Matrix3'3 a) where+ show (M3'3 a b c d e f g h i) = "(M3'3 " ++ body ++ ")" where+ body = show [[a,b,c],[d,e,f],[g,h,i]]++instance Num a => Num (Matrix3'3 a) where+ (+) = lift2Matrix3'3 (+) + (-) = lift2Matrix3'3 (-)++ (*) (M3'3 a b c d e f g h i) (M3'3 m n o p q r s t u) = + M3'3 (a*m+b*p+c*s) (a*n+b*q+c*t) (a*o+b*r+c*u) + (d*m+e*p+f*s) (d*n+e*q+f*t) (d*o+e*r+f*u) + (g*m+h*p+i*s) (g*n+h*q+i*t) (g*o+h*r+i*u) + + abs = fmap abs + negate = fmap negate+ signum = fmap signum+ fromInteger a = M3'3 a' a' a' a' a' a' a' a' a' where a' = fromInteger a ++-- Radians++instance Show Radian where+ showsPrec i (Radian a) = showsPrec i a++instance Eq Radian where (==) = req++instance Ord Radian where+ compare a b | a `req` b = EQ+ | otherwise = getRadian a `compare` getRadian b++--------------------------------------------------------------------------------+-- Pretty printing++instance Pretty a => Pretty (Vec2 a) where+ pretty (V2 a b) = angles (char '|' <+> pretty a <+> pretty b <+> char '|')++instance Pretty a => Pretty (Point2 a) where+ pretty (P2 a b) = brackets (char '|' <+> pretty a <+> pretty b <+> char '|')++instance Pretty a => Pretty (Frame2 a) where+ pretty (Frame2 e0 e1 o) = braces $+ text "e0:" <> pretty e0+ <+> text "e1:" <> pretty e1+ <+> text "o:" <> pretty o++instance PSUnit a => Pretty (Matrix3'3 a) where+ pretty (M3'3 a b c d e f g h i) = + matline a b c <$> matline d e f <$> matline g h i+ where+ matline x y z = char '|' + <+> (hcat $ map (fill 12 . text . dtrunc) [x,y,z]) + <+> char '|' +++instance Pretty Radian where+ pretty (Radian d) = double d <> text ":rad"++--------------------------------------------------------------------------------+-- Vector space and related instances++instance Num a => AdditiveGroup (Vec2 a) where+ zeroV = V2 0 0 + (^+^) = lift2Vec2 (+) + negateV = fmap negate +++instance Num a => VectorSpace (Vec2 a) where+ type Scalar (Vec2 a) = a+ s *^ v = fmap (s*) v+++-- scalar (dot / inner) product via the class InnerSpace++instance (Num a, InnerSpace a, Scalar a ~ a) + => InnerSpace (Vec2 a) where+ (V2 a b) <.> (V2 a' b') = (a <.> a') ^+^ (b <.> b')+++instance Num a => AffineSpace (Point2 a) where+ type Diff (Point2 a) = Vec2 a+ (P2 a b) .-. (P2 x y) = V2 (a-x) (b-y)+ (P2 a b) .+^ (V2 vx vy) = P2 (a+vx) (b+vy)+++instance Num a => AdditiveGroup (Matrix3'3 a) where+ zeroV = fromInteger 0+ (^+^) = (+)+ negateV = negate+++instance Num a => VectorSpace (Matrix3'3 a) where+ type Scalar (Matrix3'3 a) = a+ s *^ m = fmap (s*) m ++--------------------------------------------------------------------------------++-- | Pointwise is a Functor like type class, except that the +-- container/element relationship is defined by a type family +-- rather than a type parameter. This means that applied function +-- must be type preserving.+++class Pointwise sh where+ type Pt sh :: *+ pointwise :: (Pt sh -> Pt sh) -> sh -> sh+++instance Pointwise (a -> a) where+ type Pt (a->a) = a+ pointwise f pf = \a -> pf (f a)++instance Pointwise a => Pointwise [a] where + type Pt [a] = Pt a+ pointwise f pts = map (pointwise f) pts ++instance Pointwise (Vec2 a) where+ type Pt (Vec2 a) = Vec2 a+ pointwise f v = f v++instance Pointwise (Point2 a) where+ type Pt (Point2 a) = Point2 a+ pointwise f pt = f pt++--------------------------------------------------------------------------------++instance Ord a => CMinMax (Point2 a) where+ cmin (P2 x y) (P2 x' y') = P2 (min x x') (min y y')+ cmax (P2 x y) (P2 x' y') = P2 (max x x') (max y y')++--------------------------------------------------------------------------------+-- Matrix multiply++infixr 7 *# ++class MatrixMult mat t where + type MatrixParam t :: *+ (*#) :: MatrixParam t ~ a => mat a -> t -> t+++-- Matrix multiplication of points and vectors as per homogeneous +-- coordinates (we don't perform the /last three/ multiplications+-- as we throw the result away).+ +instance Num a => MatrixMult Matrix3'3 (Vec2 a) where+ type MatrixParam (Vec2 a) = a + (M3'3 a b c d e f _ _ _) *# (V2 m n) = V2 (a*m+b*n+c*0) (d*m+e*n+f*0)+++instance Num a => MatrixMult Matrix3'3 (Point2 a) where+ type MatrixParam (Point2 a) = a+ (M3'3 a b c d e f _ _ _) *# (P2 m n) = P2 (a*m+b*n+c*1) (d*m+e*n+f*1)++--------------------------------------------------------------------------------+-- Vectors++-- | Construct a vector with horizontal displacement.+hvec :: Num a => a -> Vec2 a+hvec d = V2 d 0++-- | Construct a vector with vertical displacement.+vvec :: Num a => a -> Vec2 a+vvec d = V2 0 d++-- | Construct a vector from an angle and magnitude.+avec :: Floating a => Radian -> a -> Vec2 a+avec theta d = V2 x y where+ ang = fromRadian theta+ x = d * cos ang+ y = d * sin ang+++--------------------------------------------------------------------------------+-- Points++zeroPt :: Num a => Point2 a+zeroPt = P2 0 0++langle :: (Floating u, Real u) => Point2 u -> Point2 u -> Radian+langle (P2 x y) (P2 x' y') = toRadian $ atan $ (y'-y) / (x'-x) +++--------------------------------------------------------------------------------+-- Frame operations++ortho :: Num a => Point2 a -> Frame2 a+ortho o = Frame2 (V2 1 0) (V2 0 1) o++displaceOrigin :: Num a => Vec2 a -> Frame2 a -> Frame2 a+displaceOrigin v (Frame2 e0 e1 o) = Frame2 e0 e1 (o.+^v)+++pointInFrame :: Num a => Point2 a -> Frame2 a -> Point2 a+pointInFrame (P2 x y) (Frame2 vx vy o) = (o .+^ (vx ^* x)) .+^ (vy ^* y) ++-- | Concatenate the elements of the frame as columns forming a+-- 3x3 matrix. Points and vectors are considered homogeneous +-- coordinates - triples where the least element is either 0 +-- indicating a vector or 1 indicating a point:+--+-- > Frame (V2 e0x e0y) (V2 e1x e1y) (P2 ox oy)+-- +-- becomes+--+-- > (M3'3 e0x e1x ox+-- > e0y e1y oy+-- > 0 0 1 )+--++frame2Matrix :: Num a => Frame2 a -> Matrix3'3 a+frame2Matrix (Frame2 (V2 e0x e0y) (V2 e1x e1y) (P2 ox oy)) = + M3'3 e0x e1x ox + e0y e1y oy + 0 0 1+++-- | Interpret the matrix as columns forming a frame.+--+-- > (M3'3 e0x e1x ox+-- > e0y e1y oy+-- > 0 0 1 )+--+-- becomes+--+-- > Frame (V2 e0x e0y) (V2 e1x e1y) (P2 ox oy)+-- ++matrix2Frame :: Matrix3'3 a -> Frame2 a+matrix2Frame (M3'3 e0x e1x ox + e0y e1y oy+ _ _ _ ) = Frame2 (V2 e0x e0y) (V2 e1x e1y) (P2 ox oy)+++-- | /Multiplication/ of frames to form their product.+frameProduct :: (Num a, InnerSpace (Vec2 a)) => Frame2 a -> Frame2 a -> Frame2 a+frameProduct = matrix2Frame `oo` on (*) frame2Matrix++++-- | Is the origin at (0,0) and are the basis vectors orthogonal +-- with unit length?+standardFrame :: Num a => Frame2 a -> Bool+standardFrame (Frame2 (V2 1 0) (V2 0 1) (P2 0 0)) = True+standardFrame _ = False+++--------------------------------------------------------------------------------+-- Matrix construction++-- | Construct the identity matrix:+--+-- > (M3'3 1 0 0+-- > 0 1 0+-- > 0 0 1 )+--++identityMatrix :: Num a => Matrix3'3 a+identityMatrix = M3'3 1 0 0 + 0 1 0 + 0 0 1++-- Common transformation matrices (for 2d homogeneous coordinates)++-- | Construct a scaling matrix:+--+-- > (M3'3 sx 0 0+-- > 0 sy 0+-- > 0 0 1 )+--++scalingMatrix :: Num a => a -> a -> Matrix3'3 a+scalingMatrix sx sy = M3'3 sx 0 0 + 0 sy 0 + 0 0 1++translationMatrix :: Num a => a -> a -> Matrix3'3 a+translationMatrix x y = M3'3 1 0 x + 0 1 y + 0 0 1++rotationMatrix :: (Floating a, Real a) => Radian -> Matrix3'3 a+rotationMatrix a = M3'3 (cos ang) (negate $ sin ang) 0 + (sin ang) (cos ang) 0 + 0 0 1+ where ang = fromRadian a++-- No reflectionMatrix function+-- A reflection about the x-axis is a scale of 1 (-1)+-- A reflection about the y-axis is a scale of (-1) 1+++-- Rotation about some /point/.+originatedRotationMatrix :: (Floating a, Real a) + => Radian -> (Point2 a) -> Matrix3'3 a+originatedRotationMatrix ang (P2 x y) = mT * (rotationMatrix ang) * mTinv+ where+ mT = M3'3 1 0 x + 0 1 y + 0 0 1++ mTinv = M3'3 1 0 (-x) + 0 1 (-y) + 0 0 1++++-- inversion++invert :: Fractional a => Matrix3'3 a -> Matrix3'3 a +invert m = (1 / determinant m) *^ adjoint m++determinant :: Num a => Matrix3'3 a -> a+determinant (M3'3 a b c d e f g h i) = a*e*i - a*f*h - b*d*i + b*f*g + c*d*h - c*e*g+++adjoint :: Num a => Matrix3'3 a -> Matrix3'3 a +adjoint = transpose . cofactor . mofm++transpose :: Matrix3'3 a -> Matrix3'3 a+transpose (M3'3 a b c + d e f + g h i) = M3'3 a d g + b e h + c f i++cofactor :: Num a => Matrix3'3 a -> Matrix3'3 a+cofactor (M3'3 a b c + d e f + g h i) = M3'3 a (-b) c+ (-d) e (-f)+ g (-h) i++mofm :: Num a => Matrix3'3 a -> Matrix3'3 a+mofm (M3'3 a b c + d e f + g h i) = M3'3 m11 m12 m13 + m21 m22 m23 + m31 m32 m33+ where + m11 = (e*i) - (f*h)+ m12 = (d*i) - (f*g)+ m13 = (d*h) - (e*g)+ m21 = (b*i) - (c*h)+ m22 = (a*i) - (c*g)+ m23 = (a*h) - (b*g)+ m31 = (b*f) - (c*e)+ m32 = (a*f) - (c*d)+ m33 = (a*e) - (b*d)++++--------------------------------------------------------------------------------+-- Radians++-- Radian numeric type++radian_epsilon :: Double+radian_epsilon = 0.0001+++req :: Radian -> Radian -> Bool+req a b = (fromRadian $ abs (a-b)) < radian_epsilon++++-- Radian construction+toRadian :: Real a => a -> Radian +toRadian = Radian . realToFrac+++-- Radian extraction +fromRadian :: Fractional a => Radian -> a+fromRadian = realToFrac . getRadian+++-- | Degrees to radians.+d2r :: (Floating a, Real a) => a -> Radian+d2r = Radian . realToFrac . (*) (pi/180)++-- | Radians to degrees.+r2d :: (Floating a, Real a) => Radian -> a+r2d = (*) (180/pi) . fromRadian+
+ src/Wumpus/Core/GraphicsState.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.GraphicsState+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD3+--+-- Maintainer : Stephen Tetley <stephen.tetley@gmail.com>+-- Stability : highly unstable+-- Portability : GHC+--+-- Data types modelling the Graphics state+--+--------------------------------------------------------------------------------+++module Wumpus.Core.GraphicsState + (+ -- * Data types + + -- ** Stroke attributes+ StrokeAttr(..)+ , LineCap(..)+ , LineJoin(..)+ , DashPattern(..)++ -- ** Font+ , FontAttr(..)+ , SVGFontStyle(..)++ -- ** Colour+ , PSRgb++ -- ** Current Translation Matrix+ , CTM(..)++ -- * Convert to CTM+ , ToCTM(..)++ -- * Convert to PSColour+ , PSColour(..)++ ) where++import Wumpus.Core.Colour+import Wumpus.Core.Geometry+import Wumpus.Core.Utils++-- Graphics state datatypes+++data StrokeAttr = LineWidth Double+ | MiterLimit Double+ | LineCap LineCap+ | LineJoin LineJoin+ | DashPattern DashPattern + deriving (Eq,Show)++data LineCap = CapButt | CapRound | CapSquare+ deriving (Enum,Eq,Show)++data LineJoin = JoinMiter | JoinRound | JoinBevel+ deriving (Enum,Eq,Show)++data DashPattern = Solid | Dash Int [Int]+ deriving (Eq,Show)+++-- PostScript (or at least GhostScript) seems to require both+-- attributes (name & size) are set at the same time.++data FontAttr = FontAttr { + font_name :: String, -- for PostScript+ svg_font_family :: String, -- for SVG+ svg_font_style :: SVGFontStyle,+ font_size :: Int + }+ deriving (Eq,Show)++data SVGFontStyle = SVG_REGULAR | SVG_BOLD | SVG_ITALIC | SVG_BOLD_ITALIC+ | SVG_OBLIQUE | SVG_BOLD_OBLIQUE+ deriving (Eq,Show)++type PSRgb = RGB3 Double++++-- | PostScript's current transformation matrix.+-- +-- PostScript and its documentation considers the matrix to be +-- in this form:+--+-- > | a b 0 |+-- > | c d 0 | +-- > | tx ty 1 |+-- +-- i.e it considers the homogeneous coordinates of an affine +-- frame as /rows/ rather than /columns/ (Wumpus uses rows, as +-- they were the usual representation in the geometry +-- presentations that inspired it).+-- +-- Using the component names that we have used in the +-- description of 'Frame2', the CTM is:+--+-- > | e0x e0y 0 |+-- > | e1x e1y 0 | +-- > | ox oy 1 |+-- +-- The CTM is represented in PostScript as an array, using our +-- names its layout is+--+-- > [ e0x e0y e1x e1y ox oy ] +--+-- Some examples, the scaling matrix:+--+-- > | sx 0 0 |+-- > | 0 sy 0 | = [ sx 0 0 sy 0 0 ]+-- > | 0 0 1 |+-- +-- Translation (displacement) :+--+-- > | 1 0 0 |+-- > | 0 1 0 | = [ 1 0 0 1 tx ty ]+-- > | tx ty 1 |+-- +-- Rotation:+-- +-- > | cos(a) sin(a) 0 |+-- > | -sin(a) cos(a) 0 | = [ cos(a) sin(a) -sin(a) cos(a) 0 0 ]+-- > | 0 0 1 |++data CTM u = CTM !u !u !u !u !u !u+ deriving (Eq,Show)++type instance DUnit (CTM u) = u++--------------------------------------------------------------------------------+-- Conversion to CTM+++class ToCTM a where + toCTM :: u ~ DUnit a => a -> CTM u++instance ToCTM (Frame2 a) where+ toCTM (Frame2 (V2 e0x e0y) (V2 e1x e1y) (P2 ox oy)) + = CTM e0x e0y e1x e1y ox oy+ ++instance ToCTM (Matrix3'3 a) where+ toCTM (M3'3 e0x e1x ox + e0y e1y oy + _ _ _ ) + = CTM e0x e0y e1x e1y ox oy+++--------------------------------------------------------------------------------+-- Conversion to PSColour++class PSColour a where psColour :: a -> RGB3 Double++instance PSColour (RGB3 Double) where+ psColour (RGB3 r g b) = RGB3 (ramp r) (ramp g) (ramp b)++instance PSColour (HSB3 Double) where+ psColour = psColour . hsb2rgb++instance PSColour (Gray Double) where+ psColour = psColour . gray2rgb++++
+ src/Wumpus/Core/OutputPostScript.hs view
@@ -0,0 +1,373 @@+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.OutputPostScript+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+--+--------------------------------------------------------------------------------++module Wumpus.Core.OutputPostScript + ( + -- * Output PostScript+ -- $fontdoc+ writePS+ , writeEPS+ + , writePS_latin1+ , writeEPS_latin1++ ) where++import Wumpus.Core.BoundingBox+import Wumpus.Core.Colour+import Wumpus.Core.Geometry+import Wumpus.Core.GraphicsState+import Wumpus.Core.PictureInternal+import Wumpus.Core.PostScript+import Wumpus.Core.TextEncoding+import Wumpus.Core.TextLatin1+import Wumpus.Core.Utils++import Data.Aviary ( appro )++import MonadLib hiding ( Label )++import Control.Monad ( mapM_, zipWithM_ )+++++++-- $fontdoc+--+-- The following fonts are expected to exist on most platforms:+--+-- > Times-Roman Times-Italic Times-Bold Times-Bolditalic+-- > Helvetica Helvetica-Oblique Helvetica-Bold Helvetica-Bold-Oblique+-- > Courier Courier-Oblique Courier-Bold Courier-Bold-Oblique+-- > Symbol+--+-- See the PostScript Language Reference Manual.++-- type FontSpec = (String,Int)++++--------------------------------------------------------------------------------+-- Render to PostScript++-- | Write a series of pictures to a Postscript file. Each +-- picture will be printed on a separate page. +--+-- If the picture contains text labels, you should provide a +-- FontSpec to transmit @findfont@, @scalefont@ etc. commands +-- to PostScript. +writePS :: (Fractional u, Ord u, PSUnit u) + => FilePath -> TextEncoder -> [Picture u] -> IO ()+writePS filepath enc pic = do + timestamp <- mkTimeStamp+ writeFile filepath $ psDraw timestamp enc pic++-- | Write a picture to an EPS (Encapsulated PostScript) file. +-- The .eps file can then be imported or embedded in another +-- document.+--+-- If the picture contains text labels, you should provide a +-- FontSpec to transmit @findfont@, @scalefont@ etc. commands +-- to PostScript. +writeEPS :: (Fractional u, Ord u, PSUnit u) + => FilePath -> TextEncoder -> Picture u -> IO ()+writeEPS filepath enc pic = do+ timestamp <- mkTimeStamp+ writeFile filepath $ epsDraw timestamp enc pic+++-- | Version of 'writePS' - using Latin1 encoding. +writePS_latin1 :: (Fractional u, Ord u, PSUnit u) + => FilePath -> [Picture u] -> IO ()+writePS_latin1 filepath = writePS filepath latin1Encoder ++-- | Version of 'writeEPS' - using Latin1 encoding. +writeEPS_latin1 :: (Fractional u, Ord u, PSUnit u) + => FilePath -> Picture u -> IO ()+writeEPS_latin1 filepath = writeEPS filepath latin1Encoder++++++++++-- | Draw a picture, generating PostScript output.+psDraw :: (Fractional u, Ord u, PSUnit u) + => String -> TextEncoder -> [Picture u] -> PostScript+psDraw timestamp enc pics = runWumpus enc $ do+ psHeader 1 timestamp+ zipWithM_ psDrawPage pages pics+ psFooter+ where+ pages = map (\i -> (show i,i)) [1..]+++psDrawPage :: (Fractional u, Ord u, PSUnit u) + => (String,Int) -> Picture u -> WumpusM ()+psDrawPage (lbl,ordinal) pic = do+ dsc_Page lbl ordinal+ ps_gsave+ cmdtrans+ outputPicture pic+ ps_grestore+ ps_showpage+ where+ (_,mbv) = repositionProperties pic+ cmdtrans = maybe (return ()) (\(V2 x y) -> ps_translate x y) mbv+ +++-- Note the bounding box may have negative components - if it +-- does it will need translating.++epsDraw :: (Fractional u, Ord u, PSUnit u) + => String -> TextEncoder -> Picture u -> PostScript+epsDraw timestamp enc pic = runWumpus enc $ do + epsHeader bb timestamp + ps_gsave+ cmdtrans+ outputPicture pic+ ps_grestore+ epsFooter + where+ (bb,mbv) = repositionProperties pic+ cmdtrans = maybe (return ()) (\(V2 x y) -> ps_translate x y) mbv+ ++psHeader :: Int -> String -> WumpusM ()+psHeader pagecount timestamp = do+ bang_PS+ dsc_Pages pagecount+ dsc_CreationDate $ parens timestamp+ dsc_EndComments+++epsHeader :: PSUnit u => BoundingBox u -> String -> WumpusM ()+epsHeader bb timestamp = do+ bang_EPS+ dsc_BoundingBox llx lly urx ury+ dsc_CreationDate $ parens timestamp+ dsc_EndComments+ where+ (llx,lly,urx,ury) = getBounds bb++getBounds :: Num u => BoundingBox u -> (u,u,u,u)+getBounds (BBox (P2 llx lly) (P2 urx ury)) = (llx,lly,urx,ury)++psFooter :: WumpusM ()+psFooter = dsc_EOF+++epsFooter :: WumpusM ()+epsFooter = do+ ps_showpage+ dsc_EOF++-- Create margins at the left and bottom of 4 points...+++-- | outputPicture +-- Frame changes, representing scalings translation, rotations...+-- are drawn when they are encountered as a @concat@ statement in a +-- block of @gsave ... grestore@.++outputPicture :: (Fractional u, PSUnit u) => Picture u -> WumpusM ()+outputPicture (PicBlank _) = return ()+outputPicture (Single (fr,_) prim) = + updateFrame fr $ outputPrimitive prim+outputPicture (Picture (fr,_) ones) = do+ updateFrame fr $ onesmapM_ outputPicture ones+outputPicture (Clip (fr,_) cp p) = + updateFrame fr $ do { clipPath cp ; outputPicture p }+++-- | @updateFrame@ relies on the current frame, when translated+-- to a matrix being invertible.+--+-- This is an allowable optimization because the current frame+-- is only manipulated with the affine transformations (scalings, +-- rotations...) which are invertible. +-- +-- It also performs another optimization:+--+-- If the frame is the standard frame @ [1 0 0 1 0 0] @ then +-- the monadic action is run as-is rather than being nested+-- in a block:+-- +-- > [1 0 0 1 0 0] concat+-- > ...+-- > [1 0 0 1 0 0] concat+--++updateFrame :: (Fractional u, PSUnit u) => Frame2 u -> WumpusM () -> WumpusM ()+updateFrame frm ma + | standardFrame frm = ma+ | otherwise = let m1 = frame2Matrix frm in + do { ps_concat $ toCTM m1+ ; ma + ; ps_concat $ toCTM $ invert m1+ }++outputPrimitive :: (Fractional u, PSUnit u) => Primitive u -> WumpusM ()+outputPrimitive (PPath (c,dp) p) = outputPath dp c p +outputPrimitive (PLabel props l) = updateFont props $ outputLabel l+outputPrimitive (PEllipse (c,dp) ct hw hh) = outputEllipse dp c ct hw hh++updateFont :: LabelProps -> WumpusM () -> WumpusM ()+updateFont (c,fnt) ma = updateColour c $ do + mb_fnt <- deltaFontAttr fnt+ maybe (return ()) fontCommand mb_fnt+ ma+ +++updateColour :: PSColour c => c -> WumpusM () -> WumpusM ()+updateColour c ma = let rgbc = psColour c in do + mb_col <- deltaRgbColour rgbc+ maybe (return ()) colourCommand mb_col+ ma+ where+ colourCommand :: DRGB -> WumpusM ()+ colourCommand (RGB3 r g b) = ps_setrgbcolor r g b+ + +++fontCommand :: FontAttr -> WumpusM ()+fontCommand (FontAttr name _ _ sz) = do+ ps_findfont name+ ps_scalefont sz+ ps_setfont++++ +outputPath :: (PSColour c, PSUnit u) + => DrawPath -> c -> Path u -> WumpusM ()+outputPath CFill c p = updateColour c $ do + startPath p+ ps_closepath+ ps_fill++outputPath (CStroke xs) c p = updatePen c xs $ do+ startPath p+ ps_closepath+ ps_stroke++outputPath (OStroke xs) c p = updatePen c xs $ do+ startPath p+ ps_stroke+ ++startPath :: PSUnit u => Path u -> WumpusM ()+startPath (Path (P2 x y) xs) = do+ ps_newpath+ ps_moveto x y+ mapM_ outputPathSeg xs++++clipPath :: PSUnit u => Path u -> WumpusM ()+clipPath p = do + startPath p+ ps_closepath+ ps_clip++++updatePen :: PSColour c => c -> [StrokeAttr] -> WumpusM () -> WumpusM ()+updatePen c xs ma = let (mset, mreset) = strokeSetReset xs in + updateColour c $ do { mset ; ma ; mreset }++strokeSetReset :: [StrokeAttr] -> (WumpusM (), WumpusM ())+strokeSetReset = foldr (appro link cmd id) (return (), return ())+ where+ link Nothing funs = funs + link (Just (f,g)) (fs,gs) = (fs >> f, gs >> g)+ + mkSetReset mf = maybe Nothing (\(a,b) -> Just (mf a, mf b))+ + cmd (LineWidth d) = mkSetReset ps_setlinewidth $ deltaStrokeWidth d+ cmd (MiterLimit d) = mkSetReset ps_setmiterlimit $ deltaMiterLimit d+ cmd (LineCap lc) = mkSetReset ps_setlinecap $ deltaLineCap lc+ cmd (LineJoin lj) = mkSetReset ps_setlinejoin $ deltaLineJoin lj+ cmd (DashPattern dp) = mkSetReset ps_setdash $ deltaDashPattern dp+++outputPathSeg :: PSUnit u => PathSegment u -> WumpusM ()+outputPathSeg (PLine (P2 x y)) = ps_lineto x y+outputPathSeg (PCurve p1 p2 p3) = ps_curveto x1 y1 x2 y2 x3 y3 + where+ P2 x1 y1 = p1+ P2 x2 y2 = p2+ P2 x3 y3 = p3++-- | This is not very good as it uses a PostScript's+-- @scale@ operator - this will vary the line width during the+-- drawing of a stroked ellipse.+outputEllipse :: (PSColour c, Fractional u, PSUnit u)+ => DrawEllipse -> c -> Point2 u -> u -> u -> WumpusM ()+outputEllipse dp c (P2 x y) hw hh + | hw==hh = outputArc dp c x y hw+ | otherwise = do { ps_gsave+ -- Not so good -- the next line changes stroke width...+ ; ps_scale 1 (hh/hw)+ ; outputArc dp c x y hw+ ; ps_grestore+ }++outputArc :: (PSColour c, PSUnit u) + => DrawEllipse -> c -> u -> u -> u -> WumpusM ()+outputArc EFill c x y r = updateColour c $ do + ps_arc x y r 0 360 + ps_closepath+ ps_fill++outputArc (EStroke xs) c x y r = updatePen c xs $ do + ps_arc x y r 0 360 + ps_closepath+ ps_stroke+++outputLabel :: PSUnit u => Label u -> WumpusM ()+outputLabel (Label (P2 x y) entxt) = do+ ps_moveto x y+ outputEncodedText entxt+-- ps_show str++outputEncodedText :: EncodedText -> WumpusM () +outputEncodedText = mapM_ outputTextChunk . getEncodedText++outputTextChunk :: TextChunk -> WumpusM () +outputTextChunk (SText s) = ps_show s++outputTextChunk (EscInt i) = + ask >>= \env -> maybe (failk env) ps_glyphshow $ lookupByCharCode i env+ where+ failk = missingCode i . ps_fallback ++outputTextChunk (EscStr s) = ps_glyphshow s ++missingCode :: CharCode -> GlyphName -> WumpusM ()+missingCode i fallback = do+ ps_comment $ "missing lookup for &#" ++ show i ++ ";" + ps_glyphshow fallback+ ++
+ src/Wumpus/Core/OutputSVG.hs view
@@ -0,0 +1,284 @@+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.OutputSVG+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Output SVG. +--+-- This is complicated by two differences with PostScript.+--+-- 1. The coordinate space of SVG is /origin top-left/, for +-- PostScript it is /origin bottom-left/.+-- +-- 2. Clipping in PostScript works by changing the graphics state+-- Clip a path, then all subsequent drawing be rendered only +-- when it is within the clip bounds. Clearly using clipping +-- paths within a @gsave ... grestore@ block is a good idea...+--+-- SVG uses /tagging/. A clipPath element is declared and named +-- then referenced in subsequent elements via the clip-path +-- attribute - @clip-path=\"url(#clip_path_tag)\"@.+--+--+--------------------------------------------------------------------------------++module Wumpus.Core.OutputSVG+ ( + + -- * Output SVG+ writeSVG++ , writeSVG_latin1+ + ) where++import Wumpus.Core.AffineTrans+import Wumpus.Core.Geometry+import Wumpus.Core.GraphicsState+import Wumpus.Core.PictureInternal+import Wumpus.Core.SVG+import Wumpus.Core.TextEncoding+import Wumpus.Core.TextLatin1+import Wumpus.Core.Utils++import Data.Aviary ( (#) )++import MonadLib hiding ( Label )++import Text.XML.Light++++type Clipped = Bool+++coordChange :: (Num u, Ord u, Scale t, u ~ DUnit t) => t -> t+coordChange = scale 1 (-1)+++--------------------------------------------------------------------------------++writeSVG :: (Ord u, PSUnit u) => FilePath -> TextEncoder -> Picture u -> IO ()+writeSVG filepath enc pic = + writeFile filepath $ unlines $ map ppContent $ svgDraw enc pic ++writeSVG_latin1 :: (Ord u, PSUnit u) => FilePath -> Picture u -> IO ()+writeSVG_latin1 filepath = writeSVG filepath latin1Encoder ++++svgDraw :: (Ord u, PSUnit u) => TextEncoder -> Picture u -> [Content]+svgDraw enc pic = runSVG enc $ + picture False pic' >>= return . topLevelPic mbvec >>= prefixXmlDecls+ where+ pic' = coordChange pic+ (_,mbvec) = repositionProperties pic'+++prefixXmlDecls :: Element -> SvgM [Content]+prefixXmlDecls e = do + enc <- asks svg_encoding_name+ let xmlv = xmlVersion enc+ return $ [Text xmlv, Text svgDocType, Elem e] ++topLevelPic :: PSUnit u => Maybe (Vec2 u) -> Element -> Element+topLevelPic Nothing p = svgElement [p]+topLevelPic (Just (V2 x y)) p = svgElement [gElement [trans_attr] [p]] + where + trans_attr = attr_transform $ val_translate x y++++picture :: (Ord u, PSUnit u) => Clipped -> Picture u -> SvgM Element+picture _ (PicBlank _) = return $ gElement [] []+picture c (Single (fr,_) prim) = do + elt <- primitive c prim+ return $ gElement (maybe [] return $ frameChange fr) [elt]++picture c (Picture (fr,_) ones) = do+ es <- toListWithM (picture c) ones+ return $ gElement (maybe [] return $ frameChange fr) es++picture _ (Clip (fr,_) p a) = do + cp <- clipPath p+ e1 <- picture True a+ return $ gElement (maybe [] return $ frameChange fr) [cp,e1]+++primitive :: (Ord u, PSUnit u) => Clipped -> Primitive u -> SvgM Element+primitive c (PPath props p) = clipAttrib c $ path props p+primitive c (PLabel props l) = clipAttrib c $ label props l+primitive c (PEllipse props mid hw hh) = clipAttrib c $ + ellipse props mid hw hh++++-- All clipping paths are closed.+clipPath :: PSUnit u => Path u -> SvgM Element+clipPath p = do+ name <- newClipLabel+ return $ element_clippath ps # add_attr (attr_id name)+ where+ ps = closePath $ pathInstructions p++++clipAttrib :: Clipped -> SvgM Element -> SvgM Element+clipAttrib False melt = melt+clipAttrib True melt = do + s <- currentClipLabel+ elt <- melt+ return $ add_attr (attr_clippath s) elt+++-- None of the remaining translation functions need to be in the+-- SvgM monad.++path :: PSUnit u => PathProps -> Path u -> SvgM Element+path (c,dp) p = + return $ element_path ps # add_attrs (fill_a : stroke_a : opts)+ where+ (fill_a,stroke_a,opts) = drawProperties c dp+ ps = svgPath dp p +++-- Labels need the coordinate system remapping otherwise+-- the will be printed upside down. Both the start point and +-- the label itself need transforming.+-- +-- Also rendering coloured text is convoluted (needing the+-- tspan element).+-- +label :: (Ord u, PSUnit u) => LabelProps -> Label u -> SvgM Element+label (c,FontAttr _ fam style sz) (Label pt entxt) = do + str <- encodedText entxt+ let tspan_elt = element_tspan str # add_attrs [ attr_fill c ]+ return $ element_text tspan_elt # add_attrs text_xs + # add_attrs (fontStyle style)+ where+ P2 x y = coordChange pt+ text_xs = [ attr_x x+ , attr_y y + , attr_transform $ val_matrix 1 0 0 (-1) 0 (0::Double)+ , attr_font_family fam+ , attr_font_size sz + ]+ + +++encodedText :: EncodedText -> SvgM String +encodedText entxt = + let xs = getEncodedText entxt in mapM textChunk xs >>= return . concat++-- | Unfortunately we can\'t readily put a comment in the +-- generated SVG when glyph-name lookup fails. Doing similar in +-- PostScript is easy because we are emiting /linear/ PostScript +-- as we go along. For SVG we are building an abstract syntax +-- tree.+-- +textChunk :: TextChunk -> SvgM String+textChunk (SText s) = return s+textChunk (EscInt i) = return $ escapeCharCode i+textChunk (EscStr s) = + asks (lookupByGlyphName s) >>= maybe failk (return . escapeCharCode) + where+ failk = asks svg_fallback >>= return . escapeCharCode ++escapeCharCode :: CharCode -> String+escapeCharCode i = "&#" ++ show i ++ ";"++ +fontStyle :: SVGFontStyle -> [Attr]+fontStyle SVG_REGULAR = []+fontStyle SVG_BOLD = [attr_font_weight "bold"]+fontStyle SVG_ITALIC = [attr_font_style "italic"]+fontStyle SVG_BOLD_ITALIC = + [attr_font_weight "bold", attr_font_style "italic"]+fontStyle SVG_OBLIQUE = [attr_font_style "oblique"]+fontStyle SVG_BOLD_OBLIQUE = + [attr_font_weight "bold", attr_font_style "oblique"]++-- If w==h the draw the ellipse as a circle++ellipse :: PSUnit u => EllipseProps -> Point2 u -> u -> u -> SvgM Element+ellipse (c,dp) (P2 x y) w h + | w == h = return $ element_circle + # add_attrs (circle_attrs ++ style_attrs)+ | otherwise = return $ element_ellipse + # add_attrs (ellipse_attrs ++ style_attrs)+ where+ circle_attrs = [attr_cx x, attr_cy y, attr_r w]+ ellipse_attrs = [attr_cx x, attr_cy y, attr_rx w, attr_ry h]+ style_attrs = fill_a : stroke_a : opts+ where (fill_a,stroke_a,opts) = drawEllipse c dp+++-- A rule of thumb seems to be that SVG (at least SVG in Firefox)+-- will try to fill unless told not to. So always label paths+-- with @fill=...@ even if fill is @\"none\"@.+--+-- CFill ==> stroke="none" fill="..."+-- CStroke ==> stroke="..." fill="none"+-- OStroke ==> stroke="..." fill="none"+--++drawProperties :: PSColour c => c -> DrawPath -> (Attr, Attr, [Attr])+drawProperties = fn where+ fn c CFill = (attr_fill c, attr_stroke_none, [])+ fn c (OStroke xs) = (attr_fill_none, attr_stroke c, strokeAttributes xs)+ fn c (CStroke xs) = (attr_fill_none, attr_stroke c, strokeAttributes xs)++drawEllipse :: PSColour c => c -> DrawEllipse -> (Attr, Attr, [Attr])+drawEllipse = fn where+ fn c EFill = (attr_fill c, attr_stroke_none, [])+ fn c (EStroke xs) = (attr_fill_none, attr_stroke c, strokeAttributes xs)+ ++strokeAttributes :: [StrokeAttr] -> [Attr]+strokeAttributes = foldr fn [] where+ fn (LineWidth a) = (:) (attr_stroke_width a)+ fn (MiterLimit a) = (:) (attr_stroke_miterlimit a)+ fn (LineCap lc) = (:) (attr_stroke_linecap lc)+ fn (LineJoin lj) = (:) (attr_stroke_linejoin lj)+ fn (DashPattern dp) = dash dp where+ dash Solid = (:) (attr_stroke_dasharray_none)+ dash (Dash _ []) = (:) (attr_stroke_dasharray_none)+ dash (Dash i xs) = (:) (attr_stroke_dashoffset i) . + (:) (attr_stroke_dasharray xs)+ +++svgPath :: PSUnit u => DrawPath -> Path u -> SvgPath+svgPath (OStroke _) p = pathInstructions p+svgPath _ p = closePath $ pathInstructions p+++pathInstructions :: PSUnit u => Path u -> [String]+pathInstructions (Path (P2 x y) xs) = path_m x y : map pathSegment xs++pathSegment :: PSUnit u => PathSegment u -> String+pathSegment (PLine (P2 x1 y1)) = path_l x1 y1+pathSegment (PCurve (P2 x1 y1) (P2 x2 y2) (P2 x3 y3)) = + path_s x1 y1 x2 y2 x3 y3++++frameChange :: PSUnit u => Frame2 u -> Maybe Attr+frameChange fr + | standardFrame fr = Nothing+ | otherwise = Just $ attr_transform $ val_matrix a b c d e f + where+ CTM a b c d e f = toCTM fr++++closePath :: SvgPath -> SvgPath +closePath xs = xs ++ ["Z"]
+ src/Wumpus/Core/Picture.hs view
@@ -0,0 +1,443 @@+{-# LANGUAGE FlexibleInstances #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.Picture+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+--+--------------------------------------------------------------------------------++module Wumpus.Core.Picture + (+ + -- * Construction+ blankPicture+ , frame+ , frameWithin+ , frameMulti+ , multi+++ , path+ , lineTo+ , curveTo+ , vertexPath + , curvedPath++ -- * Constructing primitives+ , Stroke(..)+ , zostroke+ , zcstroke++ , Fill(..)+ , zfill+ + , clip++ , TextLabel(..)+ , ztextlabel+ , multilabel++ , Ellipse(..)+ , zellipse+++++ -- * Operations+ , extendBoundary++ ) where++import Wumpus.Core.BoundingBox+import Wumpus.Core.Colour+import Wumpus.Core.Geometry+import Wumpus.Core.GraphicsState+import Wumpus.Core.PictureInternal+import Wumpus.Core.TextEncoding+import Wumpus.Core.Utils++import Data.Semigroup+++++--------------------------------------------------------------------------------++-- Default attributes++psBlack :: PSRgb+psBlack = RGB3 0 0 0+ +-- aka the standard frame+stdFrame :: Num u => Frame2 u +stdFrame = ortho zeroPt++--------------------------------------------------------------------------------+-- OneList helper+++-- | This module (Wumpus.Core.Picture) should be the only +-- interface to the /outside world/ for creating ++--------------------------------------------------------------------------------+-- Construction++++-- | Create a blank picture sized to the supplied bounding box.+-- This is useful for spacing rows or columns of pictures.+blankPicture :: Num u => BoundingBox u -> Picture u+blankPicture bb = PicBlank (stdFrame, bb)+++-- | Lift a Primitive to a Picture, located in the standard frame.+frame :: (Fractional u, Ord u) => Primitive u -> Picture u+frame p = Single (stdFrame, boundary p) p ++-- | Frame a picture within the supplied bounding box+-- +-- A text label uses the supplied bounding box as is - no +-- clipping is performed if the bounding box is +-- smaller than the boundary size of the text. This may +-- cause strange overlap for subsequent composite pictures, and+-- incorrect bounding box annotations in the prologue of the +-- generated EPS file. +-- +-- Paths and ellipses are bound within the union of the supplied +-- bounding box and the inherent bounding box or the path or +-- ellipse. Thus the bounding box will never reframed to a +-- smaller size than the /natural/ bounding box.+--+frameWithin :: (Fractional u, Ord u) => Primitive u -> BoundingBox u -> Picture u+frameWithin p@(PLabel _ _) bb = Single (stdFrame,bb) p+frameWithin p bb = Single (stdFrame,bb `append` boundary p) p+++++-- | Lift a list of Primitives to a composite Picture, all +-- Primitives will be located within the standard frame.+-- The list of Primitives must be non-empty.+--+frameMulti :: (Fractional u, Ord u) => [Primitive u] -> Picture u+frameMulti [] = error "Wumpus.Core.Picture.frameMulti - empty list"+frameMulti xs = multi $ map frame xs+++-- | Place multiple pictures within the same affine frame+-- This function throws an error when supplied the empty list.+multi :: (Fractional u, Ord u) => [Picture u] -> Picture u+multi ps = Picture (stdFrame, sconcat $ map boundary ps) ones+ where + sconcat [] = error err_msg+ sconcat (x:xs) = foldr append x xs++ ones = fromListErr err_msg ps++ err_msg = "Wumpus.Core.Picture.multi - empty list"++++-- | Create a Path from the start point and alist of PathSegments.+path :: Point2 u -> [PathSegment u] -> Path u+path = Path ++-- | Create a straight-line PathSegment.+lineTo :: Point2 u -> PathSegment u+lineTo = PLine++-- | Create a curved PathSegment.+curveTo :: Point2 u -> Point2 u -> Point2 u -> PathSegment u+curveTo = PCurve+++-- | Convert the list of vertices to a path of straight line +-- segments.+vertexPath :: [Point2 u] -> Path u+vertexPath [] = error "Picture.vertexPath - empty point list"+vertexPath (x:xs) = Path x (map PLine xs)+++-- Not a paramorphism as you want to consume3 rather than +-- look-ahead3...++-- | Convert a list of vertices to a path of curve segments.+-- The first point in the list makes the start point, each curve +-- segment thereafter takes 3 points. /Spare/ points at the end +-- are discarded. +curvedPath :: [Point2 u] -> Path u+curvedPath [] = error "Picture.curvedPath - empty point list"+curvedPath (x:xs) = Path x (fn xs) where+ fn (a:b:c:ys) = PCurve a b c : fn ys + fn _ = []+++ ++++--------------------------------------------------------------------------------+-- Take Paths to Primitives+++ostrokePath :: (Num u, Ord u) + => PSRgb -> [StrokeAttr] -> Path u -> Primitive u+ostrokePath c attrs p = PPath (c, OStroke attrs) p++cstrokePath :: (Num u, Ord u) + => PSRgb -> [StrokeAttr] -> Path u -> Primitive u+cstrokePath c attrs p = PPath (c, CStroke attrs) p++class Stroke t where+ ostroke :: (Num u, Ord u) => t -> Path u -> Primitive u+ cstroke :: (Num u, Ord u) => t -> Path u -> Primitive u++instance Stroke () where+ ostroke () = ostrokePath psBlack []+ cstroke () = cstrokePath psBlack []++instance Stroke (RGB3 Double) where+ ostroke c = ostrokePath (psColour c) []+ cstroke c = cstrokePath (psColour c) []++instance Stroke (HSB3 Double) where+ ostroke c = ostrokePath (psColour c) []+ cstroke c = cstrokePath (psColour c) []++instance Stroke (Gray Double) where+ ostroke c = ostrokePath (psColour c) []+ cstroke c = cstrokePath (psColour c) []+++instance Stroke StrokeAttr where+ ostroke x = ostrokePath psBlack [x]+ cstroke x = cstrokePath psBlack [x]++instance Stroke [StrokeAttr] where+ ostroke xs = ostrokePath psBlack xs+ cstroke xs = cstrokePath psBlack xs++++instance Stroke (RGB3 Double,StrokeAttr) where+ ostroke (c,x) = ostrokePath (psColour c) [x]+ cstroke (c,x) = cstrokePath (psColour c) [x]++instance Stroke (HSB3 Double,StrokeAttr) where+ ostroke (c,x) = ostrokePath (psColour c) [x]+ cstroke (c,x) = cstrokePath (psColour c) [x]++instance Stroke (Gray Double,StrokeAttr) where+ ostroke (c,x) = ostrokePath (psColour c) [x]+ cstroke (c,x) = cstrokePath (psColour c) [x]++instance Stroke (RGB3 Double,[StrokeAttr]) where+ ostroke (c,xs) = ostrokePath (psColour c) xs+ cstroke (c,xs) = cstrokePath (psColour c) xs++instance Stroke (HSB3 Double,[StrokeAttr]) where+ ostroke (c,xs) = ostrokePath (psColour c) xs+ cstroke (c,xs) = cstrokePath (psColour c) xs++instance Stroke (Gray Double,[StrokeAttr]) where+ ostroke (c,xs) = ostrokePath (psColour c) xs+ cstroke (c,xs) = cstrokePath (psColour c) xs+++-- | Create an open stoke coloured black.+zostroke :: (Num u, Ord u) => Path u -> Primitive u+zostroke = ostrokePath psBlack []+ +-- | Create a closed stroke coloured black.+zcstroke :: (Num u, Ord u) => Path u -> Primitive u+zcstroke = cstrokePath psBlack []+++++-- fills only have one property - colour+-- Having a fill class seems uniform as we have a stroke class ++++fillPath :: (Num u, Ord u) => PSRgb -> Path u -> Primitive u+fillPath c p = PPath (c,CFill) p++class Fill t where+ fill :: (Num u, Ord u) => t -> Path u -> Primitive u+ ++instance Fill () where fill () = fillPath psBlack +instance Fill (RGB3 Double) where fill = fillPath . psColour+instance Fill (HSB3 Double) where fill = fillPath . psColour+instance Fill (Gray Double) where fill = fillPath . psColour++-- | Create a filled path coloured black. +zfill :: (Num u, Ord u) => Path u -> Primitive u+zfill = fillPath psBlack++--------------------------------------------------------------------------------+-- Clipping ++clip :: (Num u, Ord u) => Path u -> Picture u -> Picture u+clip cp p = Clip (ortho zeroPt, boundary cp) cp p+++--------------------------------------------------------------------------------+-- Labels to primitive++mkTextLabel :: PSRgb -> FontAttr -> Point2 u -> String -> Primitive u+mkTextLabel c attr pt txt = PLabel (c,attr) (Label pt $ lexLabel txt)++-- SVG seems to have an issue with /Courier/ and needs /Courier New/.++default_font :: FontAttr+default_font = FontAttr "Courier" "Courier New" SVG_REGULAR 12++class TextLabel t where + textlabel :: t -> Point2 u -> String -> Primitive u+++instance TextLabel () where textlabel () = mkTextLabel psBlack default_font++instance TextLabel (RGB3 Double) where+ textlabel c = mkTextLabel (psColour c) default_font++instance TextLabel (HSB3 Double) where+ textlabel c = mkTextLabel (psColour c) default_font++instance TextLabel (Gray Double) where+ textlabel c = mkTextLabel (psColour c) default_font++instance TextLabel FontAttr where+ textlabel a = mkTextLabel psBlack a++instance TextLabel (RGB3 Double,FontAttr) where+ textlabel (c,a) = mkTextLabel (psColour c) a++instance TextLabel (HSB3 Double,FontAttr) where+ textlabel (c,a) = mkTextLabel (psColour c) a++instance TextLabel (Gray Double,FontAttr) where+ textlabel (c,a) = mkTextLabel (psColour c) a++-- | Create a label where the font is @Courier@, text size is 10 +-- and colour is black.+ztextlabel :: Point2 u -> String -> Primitive u+ztextlabel = mkTextLabel psBlack default_font++++-- (The implementation of this function needs attention).+--+multilabel :: (Fractional u, Ord u) + => [Label u] -> LabelProps -> BoundingBox u -> Picture u+multilabel ps props bb = + Picture (stdFrame, bb) $ fromListErr err_msg + $ map frame+ $ zipWith PLabel (repeat props) ps + where + err_msg = "Wumpus.Core.Picture.multilabel - empty list."++--------------------------------------------------------------------------------++mkEllipse :: Num u + => PSRgb -> DrawEllipse -> Point2 u -> u -> u -> Primitive u+mkEllipse c dp pt hw hh = PEllipse (c,dp) pt hw hh+++ellipseDefault :: EllipseProps+ellipseDefault = (psBlack, EFill)+++-- | Instances will create a filled ellipse unless the supplied +-- element /implies/ a stoked ellipse, e.g.:+--+-- > ellipse (LineWidth 4) zeroPt 40 40 +-- > ellipse EFill zeroPt 40 40 +--+class Ellipse t where+ ellipse :: Fractional u => t -> Point2 u -> u -> u -> Primitive u++instance Ellipse () where ellipse () = zellipse+instance Ellipse DrawEllipse where ellipse dp = mkEllipse psBlack dp++instance Ellipse StrokeAttr where + ellipse = mkEllipse psBlack . EStroke . return++instance Ellipse [StrokeAttr] where + ellipse = mkEllipse psBlack . EStroke++instance Ellipse (RGB3 Double) where + ellipse c = mkEllipse (psColour c) EFill++instance Ellipse (HSB3 Double) where + ellipse c = mkEllipse (psColour c) EFill++instance Ellipse (Gray Double) where + ellipse c = mkEllipse (psColour c) EFill+++instance Ellipse (RGB3 Double,DrawEllipse) where + ellipse (c,dp) = mkEllipse (psColour c) dp++instance Ellipse (HSB3 Double,DrawEllipse) where + ellipse (c,dp) = mkEllipse (psColour c) dp++instance Ellipse (Gray Double,DrawEllipse) where + ellipse (c,dp) = mkEllipse (psColour c) dp+++instance Ellipse (RGB3 Double,StrokeAttr) where + ellipse (c,x) = mkEllipse (psColour c) (EStroke [x])++instance Ellipse (HSB3 Double,StrokeAttr) where + ellipse (c,x) = mkEllipse (psColour c) (EStroke [x])++instance Ellipse (Gray Double,StrokeAttr) where + ellipse (c,x) = mkEllipse (psColour c) (EStroke [x])++instance Ellipse (RGB3 Double,[StrokeAttr]) where + ellipse (c,xs) = mkEllipse (psColour c) (EStroke xs)++instance Ellipse (HSB3 Double,[StrokeAttr]) where + ellipse (c,xs) = mkEllipse (psColour c) (EStroke xs)++instance Ellipse (Gray Double,[StrokeAttr]) where + ellipse (c,xs) = mkEllipse (psColour c) (EStroke xs)+++-- | Create a black, filled ellipse. +zellipse :: Num u => Point2 u -> u -> u -> Primitive u+zellipse = uncurry mkEllipse ellipseDefault+++--------------------------------------------------------------------------------++-- Operations on pictures and paths+++++-- | Extend the bounding box of a picture. +--+-- The bounding box is both horizontal directions by @x@ and +-- both vertical directions by @y@. @x@ and @y@ must be positive+-- This function cannot be used to shrink a boundary.+--+extendBoundary :: (Num u, Ord u) => u -> u -> Picture u -> Picture u+extendBoundary x y = mapLocale (\(fr,bb) -> (fr, extBB (posve x) (posve y) bb)) + where+ extBB x' y' (BBox (P2 x0 y0) (P2 x1 y1)) = BBox pt1 pt2 where + pt1 = P2 (x0-x') (y0-y')+ pt2 = P2 (x1+x') (y1+y')+ + posve n | n < 0 = 0+ | otherwise = n
+ src/Wumpus/Core/PictureInternal.hs view
@@ -0,0 +1,461 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.PictureInternal+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Internal representation of Pictures +-- +--------------------------------------------------------------------------------++module Wumpus.Core.PictureInternal + (+ -- * Data types+ Picture(..)+ , DPicture+ , Primitive(..)+ , DPrimitive+ , Path(..)+ , DPath+ , PathSegment(..)+ , DPathSegment+ , Label(..)+ , DLabel++ , PathProps -- hide in Wumpus.Core export?+ , LabelProps -- hide in Wumpus.Core export?+ , EllipseProps -- + , DrawPath(..) -- hide in Wumpus.Core export?+ , DrawEllipse(..)+ , Locale + + -- * Type class++ , PSUnit(..)+ + -- * Extras+ , mapLocale+ , extractFrame+ , repositionProperties++ ) where++import Wumpus.Core.AffineTrans+import Wumpus.Core.BoundingBox+import Wumpus.Core.FontSize+import Wumpus.Core.Geometry+import Wumpus.Core.GraphicsState+import Wumpus.Core.PictureLanguage hiding ( hcat, vcat, hsep, vsep )+import Wumpus.Core.TextEncoding+import Wumpus.Core.Utils++import Data.Aviary++import Data.AffineSpace+import Data.Semigroup++import Text.PrettyPrint.Leijen++++-- | Picture is a leaf attributed tree - where atttibutes are +-- colour, line-width etc. It is parametric on the unit type +-- of points (typically Double).+-- +-- Wumpus\'s Picture, being a leaf attributed tree, is not +-- ideally matched to PostScript\'s picture representation, +-- which might be considered a node attributed tree if you +-- recast graphics state updates as syntactic commands +-- encountered during top-down evaluation.+-- +-- Currently this mismatch means that the PostScript code +-- generated by Wumpus has significant overuse of PostScript's+-- @gsave@ and @grestore@.+--+-- At some point a tree-rewriting step might be added to +-- coalesce some of the repeated graphics state updates.+--+-- Apropos the constructors, Picture is a simple non-empty +-- leaf-labelled rose tree via +-- +-- > Single (aka leaf) | Picture (OneList tree)+--+-- Where OneList is a variant of the standard list type that +-- disallows empty lists.+-- +-- The additional constructors are convenience:+--+-- @PickBlank@ has a bounding box but no content and is useful for+-- some picture language operations (e.g. @hsep@).+--+-- @Clip@ nests a picture (tree) inside a clipping path.+--+++data Picture u = PicBlank (Locale u)+ | Single (Locale u) (Primitive u)+ | Picture (Locale u) (OneList (Picture u))+ | Clip (Locale u) (Path u) (Picture u)+ deriving (Eq,Show) ++type DPicture = Picture Double+++-- | Wumpus\'s drawings are built from two fundamental +-- primitives: paths (line segments and Bezier curves) and +-- labels (single lines of text). +-- +-- Ellipses are a included as a primitive only for optimization +-- - drawing a reasonable circle with Bezier curves needs at +-- least eight curves. This is inconvenient for drawing dots +-- which can otherwise be drawn with a single @arc@ command.+-- +-- Wumpus does not follow PostScript and employ arcs as general +-- path primitives - they are used only to draw ellipses. This +-- is because arcs do not enjoy the nice properties of Bezier +-- curves, whereby the affine transformation of a Bezier curve +-- can simply be achieved by the affine transformation of it\'s +-- control points.+--+-- Ellipses are represented by their center, half-width and +-- half-height. Half-width and half-height are used so the +-- bounding box can be calculated using only multiplication, and +-- thus initially only obliging a Num constraint on the unit.+-- Though typically for affine transformations a Fractional +-- constraint is also obliged.+--++data Primitive u = PPath PathProps (Path u)+ | PLabel LabelProps (Label u) + | PEllipse { + ellipse_props :: EllipseProps,+ ellipse_center :: Point2 u,+ ellipse_half_width :: u,+ ellipse_half_height :: u + } + deriving (Eq,Show)++type DPrimitive = Primitive Double++++data Path u = Path (Point2 u) [PathSegment u]+ deriving (Eq,Show)++type DPath = Path Double+++data PathSegment u = PCurve (Point2 u) (Point2 u) (Point2 u)+ | PLine (Point2 u)+ deriving (Eq,Show)++type DPathSegment = PathSegment Double++data Label u = Label { + label_bottom_left :: Point2 u,+ label_text :: EncodedText+ }+ deriving (Eq,Show)++type DLabel = Label Double+++-- | Note when drawn /filled/ and drawn /stroked/ the same +-- polygon will have (slightly) different size: +-- +-- * A filled shape fills /within/ the boundary of the shape+-- +-- * A stroked shape draws a pen line around the boundary +-- of the shape. The actual size depends on the thickness+-- of the line (stroke width).+--+data DrawPath = CFill | CStroke [StrokeAttr] | OStroke [StrokeAttr]+ deriving (Eq,Show)++-- | Ellipses and circles are always closed.+data DrawEllipse = EFill | EStroke [StrokeAttr]+ deriving (Eq,Show)++type PathProps = (PSRgb, DrawPath)+type LabelProps = (PSRgb, FontAttr)+type EllipseProps = (PSRgb, DrawEllipse)++-- | Locale = (current frame x bounding box)+-- +-- Pictures (and sub-pictures) are located within an affine frame.+-- So pictures can be arranged (vertical and horizontal +-- composition) their bounding box is cached.+--+-- In Wumpus, affine transformations (scalings, rotations...)+-- transform the frame rather than the constituent points of +-- the primitives. Changes of frame are transmitted to PostScript+-- as @concat@ commands (and matrix transforms in SVG) - the +-- @point-in-world-coordinate@ of a point on a path is never +-- calculated.+-- +-- So that picture composition is remains stable under affine+-- transformation, the corners of bounding boxes are transformed+-- pointwise when the picture is scaled, rotated etc.+--+type Locale u = (Frame2 u, BoundingBox u) +++--------------------------------------------------------------------------------+-- Pretty printing++instance (Num u, Pretty u) => Pretty (Picture u) where+ pretty (PicBlank m) = text "*BLANK*" <+> ppLocale m+ pretty (Single m prim) = ppLocale m <$> indent 2 (pretty prim)+ pretty (Picture m ones) = + ppLocale m <$> indent 2 (list $ toListWith pretty ones)++ pretty (Clip m cpath p) = + text "Clip:" <+> ppLocale m <$> indent 2 (pretty cpath)+ <$> indent 2 (pretty p)++ppLocale :: (Num u, Pretty u) => Locale u -> Doc+ppLocale (fr,bb) = align (ppfr <$> pretty bb) where+ ppfr = if standardFrame fr then text "*std-frame*" else pretty fr+++instance Pretty u => Pretty (Primitive u) where+ pretty (PPath _ p) = pretty "path:" <+> pretty p+ pretty (PLabel _ lbl) = pretty lbl+ pretty (PEllipse _ c w h) = pretty "ellipse" <+> pretty c+ <+> text "w:" <> pretty w+ <+> text "h:" <> pretty h+++instance Pretty u => Pretty (Path u) where+ pretty (Path pt ps) = pretty pt <> hcat (map pretty ps)++instance Pretty u => Pretty (PathSegment u) where+ pretty (PCurve p1 p2 p3) = text ".*" <> pretty p1 <> text ",," <> pretty p2 + <> text "*." <> pretty p3+ pretty (PLine pt) = text "--" <> pretty pt++instance Pretty u => Pretty (Label u) where+ pretty (Label pt s) = dquotes (pretty s) <> char '@' <> pretty pt+++--------------------------------------------------------------------------------++-- | Paths are sensibly a Semigroup - there is no notion of +-- /empty path/.++instance Semigroup (Path u) where+ Path st xs `append` Path st' xs' = Path st (xs ++ (PLine st' : xs'))+++instance Pointwise (Path u) where+ type Pt (Path u) = Point2 u+ pointwise f (Path st xs) = Path (f st) (map (pointwise f) xs)++instance Pointwise (PathSegment u) where+ type Pt (PathSegment u) = Point2 u+ pointwise f (PLine p) = PLine (f p)+ pointwise f (PCurve p1 p2 p3) = PCurve (f p1) (f p2) (f p3)+ +++--------------------------------------------------------------------------------+-- Affine trans instances++type instance DUnit (Picture u) = u+type instance DUnit (Primitive u) = u+type instance DUnit (Path u) = u++instance (Floating u, Real u) => Rotate (Picture u) where+ rotate = rotatePicture ++instance (Floating u, Real u) => RotateAbout (Picture u) where+ rotateAbout = rotatePictureAbout++instance (Num u, Ord u) => Scale (Picture u) where+ scale = scalePicture++instance (Num u, Ord u) => Translate (Picture u) where+ translate = translatePicture++--------------------------------------------------------------------------------++-- Helpers for the affine transformations++rotatePicture :: (Real u, Floating u) => Radian -> Picture u -> Picture u+rotatePicture = bigphi transformPicture rotate rotate+++rotatePictureAbout :: (Real u, Floating u) + => Radian -> Point2 u -> Picture u -> Picture u+rotatePictureAbout ang pt = + transformPicture (rotateAbout ang pt) (rotateAbout ang pt)+ +scalePicture :: (Num u, Ord u) => u -> u -> Picture u -> Picture u+scalePicture x y = transformPicture (scale x y) (scale x y)++translatePicture :: (Num u, Ord u) => u -> u -> Picture u -> Picture u+translatePicture x y = transformPicture (translate x y) (translate x y)+++transformPicture :: (Num u, Ord u) + => (Point2 u -> Point2 u) + -> (Vec2 u -> Vec2 u) + -> Picture u + -> Picture u+transformPicture fp fv = + mapLocale $ \(frm,bb) -> (transformFrame fp fv frm, transformBBox fp bb)+++-- Shouldn't transforming the frame be the inverse transformation?++transformFrame :: Num u+ => (Point2 u -> Point2 u) + -> (Vec2 u -> Vec2 u) + -> Frame2 u + -> Frame2 u+transformFrame fp fv (Frame2 e0 e1 o) = Frame2 (fv e0) (fv e1) (fp o)+++-- Bounding boxes need recalculating after a transformation.+-- For instance after a reflection in the y-axis br becomes bl.+transformBBox :: (Num u, Ord u)+ => (Point2 u -> Point2 u) -> BoundingBox u -> BoundingBox u+transformBBox fp = trace . map fp . corners+++--------------------------------------------------------------------------------++-- TO DETERMINE+-- What should leftBound and rightBound be for an empty picture?++type instance PUnit (Picture u) = u++instance (Num u, Ord u) => Horizontal (Picture u) where+ moveH a = movePic (hvec a) + leftBound = leftPlane . boundary+ rightBound = rightPlane . boundary++instance (Num u, Ord u) => Vertical (Picture u) where+ moveV a = movePic (vvec a) + topBound = upperPlane . boundary+ bottomBound = lowerPlane . boundary++-- Note - picture is a binary tree and drawing is depth-first,+-- left-to-right so pictures in the right of the tree potentially+-- are drawn on top of pictures on the left.+--+-- So to print picture a _over_ picture b we form this node:+--+-- > locale +-- > /\+-- > / \+-- > b a+--+-- Hence `over` flips b and a+++instance (Num u, Ord u) => Composite (Picture u) where+ a `over` b = Picture (ortho zeroPt, bb) (mkList2 b a) where+ bb = union (boundary a) (boundary b)+ ++++instance (Num u, Ord u, Horizontal (Picture u), Vertical (Picture u)) => + Move (Picture u) where+ move x y = movePic (V2 x y)+++instance Num u => Blank (Picture u) where+ blank w h = PicBlank (ortho zeroPt, bbox zeroPt (P2 w h))++--------------------------------------------------------------------------------+-- Boundary++instance (Num u, Ord u) => Boundary (Path u) where+ boundary (Path st xs) = trace $ st : foldr f [] xs where+ f (PLine p1) acc = p1 : acc+ f (PCurve p1 p2 p3) acc = p1 : p2 : p3 : acc +++-- Note - this will calculate a very bad bounding box for text.+-- Descenders will be transgress the boundary and width will be +-- very long.++instance (Fractional u, Ord u) => Boundary (Primitive u) where+ boundary (PPath _ p) = boundary p+ boundary (PLabel (_,a) (Label pt xs)) = textBounds (font_size a) pt char_count+ where char_count = textLength xs+ boundary (PEllipse _ c hw hh) = BBox (c .-^ v) (c .+^ v) + where v = V2 hw hh+++instance Boundary (Picture u) where+ boundary (PicBlank (_,bb)) = bb+ boundary (Single (_,bb) _) = bb+ boundary (Picture (_,bb) _) = bb+ boundary (Clip (_,bb) _ _) = bb++++++--------------------------------------------------------------------------------+--+++mapLocale :: (Locale u -> Locale u) -> Picture u -> Picture u+mapLocale f (PicBlank m) = PicBlank (f m)+mapLocale f (Single m prim) = Single (f m) prim+mapLocale f (Picture m ones) = Picture (f m) ones+mapLocale f (Clip m x p) = Clip (f m) x p+++movePic :: Num u => Vec2 u -> Picture u -> Picture u+movePic v = mapLocale (moveLocale v) ++ +moveLocale :: Num u => Vec2 u -> Locale u -> Locale u+moveLocale v (fr,bb) = (displaceOrigin v fr, pointwise (.+^ v) bb) ++--------------------------------------------------------------------------------+++-- | Should this really be public?+extractFrame :: Num u => Picture u -> Frame2 u+extractFrame (PicBlank (fr,_)) = fr+extractFrame (Single (fr,_) _) = fr+extractFrame (Picture (fr,_) _) = fr+extractFrame (Clip (fr,_) _ _) = fr+++-- This needs is for PostScript and SVG output - it should be +-- hidden in the export list of Wumpus.Core+++-- If a picture has coordinates smaller than (P2 4 4) then it +-- needs repositioning before it is drawn to PostSCript or SVG.+-- +-- (P2 4 4) gives a 4 pt margin - maybe it sould be (0,0) or +-- user defined.+--+repositionProperties :: (Num u, Ord u) => Picture u -> (BoundingBox u, Maybe (Vec2 u))+repositionProperties = fn . boundary where+ fn bb@(BBox (P2 llx lly) (P2 urx ury))+ | llx < 4 || lly < 4 = (BBox ll ur, Just $ V2 x y)+ | otherwise = (bb, Nothing)+ where + x = 4 - llx+ y = 4 - lly+ ll = P2 (llx+x) (lly+y)+ ur = P2 (urx+x) (ury+y) ++
+ src/Wumpus/Core/PictureLanguage.hs view
@@ -0,0 +1,317 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.PictureLanguage+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Picture language operations c.f. PPrint and +-- Text.PrettyPrint.HughesPJ, but fully in two dimensions +-- rather than horizontal + carriage return.+--+--------------------------------------------------------------------------------++module Wumpus.Core.PictureLanguage + (+ HAlign(..)+ , VAlign(..)++ -- * Type family and classes+ , PUnit + , Horizontal(..)+ , Vertical(..)+ , Composite(..)+ , Move(..)+ , Blank(..)++ -- * Bounds+ , center+ , topleft+ , topright+ , bottomleft+ , bottomright++ -- * Composition+ , ( ->- )+ , ( -<- )+ , ( -//- )+ , ( -\\- )+ , at+ , stackOnto+ , hcat + , vcat+ , ( -@- )+ , stackOntoCenter++ , hspace+ , vspace+ , hsep+ , vsep+ + -- * Compose with alignment+ , alignH+ , alignV+ , hcatA+ , vcatA+ , hsepA+ , vsepA++ ) where++import Wumpus.Core.Geometry ( Point2(..), Vec2(..) )++import Data.AffineSpace++import Data.List ( foldl' )+++--------------------------------------------------------------------------------+-- Data types++-- Alignment+++data HAlign = HTop | HCenter | HBottom+ deriving (Eq,Show)++data VAlign = VLeft | VCenter | VRight+ deriving (Eq,Show)+++++--------------------------------------------------------------------------------+-- Type family and classes+++-- The unit type of /points/ within a Picture.+type family PUnit a+++class Horizontal a where+ moveH :: PUnit a -> a -> a+ leftBound :: a -> PUnit a+ rightBound :: a -> PUnit a++class Vertical a where+ moveV :: PUnit a -> a -> a+ topBound :: a -> PUnit a+ bottomBound :: a -> PUnit a++class Composite a where+ over :: a -> a -> a+ beneath :: a -> a -> a++ beneath = flip over+ +-- Move in 2D+class Move a where+ move :: PUnit a -> PUnit a -> a -> a+++class Blank a where+ blank :: PUnit a -> PUnit a -> a++++--------------------------------------------------------------------------------++-- Operations on bounds++-- | The center of a picture.+center :: (Horizontal a, Vertical a, Fractional u, u ~ PUnit a) => a -> Point2 u+center a = P2 hcenter vcenter where + hcenter = leftBound a + 0.5 * (rightBound a - leftBound a)+ vcenter = bottomBound a + 0.5 * (topBound a - bottomBound a)++topleft :: (Horizontal a, Vertical a, u ~ PUnit a) => a -> Point2 u+topleft a = P2 (leftBound a) (topBound a)++topright :: (Horizontal a, Vertical a, u ~ PUnit a) => a -> Point2 u+topright a = P2 (rightBound a) (topBound a)++bottomleft :: (Horizontal a, Vertical a, u ~ PUnit a) => a -> Point2 u+bottomleft a = P2 (leftBound a) (bottomBound a)++bottomright :: (Horizontal a, Vertical a, u ~ PUnit a) => a -> Point2 u+bottomright a = P2 (rightBound a) (bottomBound a)+++leftmid :: (Fractional u, Horizontal a, Vertical a, u ~ PUnit a) + => a -> Point2 u+leftmid a = P2 (leftBound a) (midpt (bottomBound a) (topBound a))++rightmid :: (Fractional u, Horizontal a, Vertical a, u ~ PUnit a) + => a -> Point2 u+rightmid a = P2 (rightBound a) (midpt (bottomBound a) (topBound a))+++topmid :: (Fractional u, Horizontal a, Vertical a, u ~ PUnit a) + => a -> Point2 u+topmid a = P2 (midpt (leftBound a) (rightBound a)) (topBound a)++bottommid :: (Fractional u, Horizontal a, Vertical a, u ~ PUnit a) + => a -> Point2 u+bottommid a = P2 (midpt (leftBound a) (rightBound a)) (bottomBound a)+++midpt :: Fractional a => a -> a -> a+midpt a b = a + 0.5*(b-a)++--------------------------------------------------------------------------------+-- Composition++infixr 5 -//-+infixr 6 ->-, -@-+++-- | Center the pic1 on top of pic2.+(-@-) :: (Horizontal a, Vertical a, Composite a, Move a, Fractional u, + u ~ PUnit a)+ => a -> a -> a+p1 -@- p2 = p1 `over` (move x y p2) where V2 x y = center p1 .-. center p2+++-- | Horizontal composition - place @b@ at the right of @a@.+(->-) :: (Horizontal a, Composite a, Num u, u ~ PUnit a) => a -> a -> a+a ->- b = over a (moveH disp b) where disp = rightBound a - leftBound b ++-- | Horizontal composition - place @a@ at the left of @b@.+(-<-) :: (Horizontal a, Composite a, Num u, u ~ PUnit a) => a -> a -> a+(-<-) = flip (->-) -- TO TEST...++-- | Vertical composition - place @b@ below @a@.+(-//-) :: (Vertical a, Composite a, Num u, u ~ PUnit a) => a -> a -> a+a -//- b = over a (moveV disp b) where disp = bottomBound a - topBound b ++-- | Vertical composition - place @a@ above @b@.+(-\\-) :: (Vertical a, Composite a, Num u, u ~ PUnit a) => a -> a -> a+(-\\-) = flip (-//-)+++-- | Place the picture at the supplied point.+at :: (Move a, u ~ PUnit a) => a -> Point2 u -> a+p `at` (P2 x y) = move x y p+++-- stackOnto :: [a] -> a -> a+-- This would obviate the need for pempty without needing a +-- non-empty list++-- | Stack the pictures using 'over' - the first picture in the +-- list is drawn at the top, last picture is on drawn at the +-- bottom.+stackOnto :: (Composite a) => [a] -> a -> a+stackOnto = flip (foldr over)++hcat :: (Horizontal a, Composite a, Num u, u ~ PUnit a)+ => a -> [a] -> a+hcat = foldl' (->-)++vcat :: (Vertical a, Composite a, Num u, u ~ PUnit a)+ => a -> [a] -> a+vcat = foldl' (-//-)++++-- | Stack pictures centered ontop of each other - the first +-- picture in the list is drawn at the top, last picture is on +-- drawn at the bottom.+stackOntoCenter :: (Horizontal a, Vertical a, Composite a, + Move a, Fractional u,+ u ~ PUnit a)+ => [a] -> a -> a+stackOntoCenter = flip $ foldr (-@-)++++--------------------------------------------------------------------------------+++blankH :: (Num u, Blank a, u ~ PUnit a) => u -> a+blankH = blank `flip` 0++blankV :: (Num u, Blank a, u ~ PUnit a) => u -> a+blankV = blank 0++++-- | The following simple definition of hspace is invalid:+--+-- > hspace n a b = a ->- (moveH n b)+-- +-- The movement due to @moveH n@ is annulled by the @->-@ +-- operator which moves relative to the bounding box.+-- +-- The almost as simple definition below, seems to justify +-- including Blank as a Picture constructor.+--+hspace :: (Num u, Composite a, Horizontal a, Blank a, u ~ PUnit a) + => u -> a -> a -> a+hspace n a b = a ->- blankH n ->- b++vspace :: (Num u, Composite a, Vertical a, Blank a, u ~ PUnit a) + => u -> a -> a -> a+vspace n a b = a -//- blankV n -//- b++hsep :: (Num u, Composite a, Horizontal a, Blank a, u ~ PUnit a) + => u -> a -> [a] -> a+hsep n = foldl' (hspace n)++vsep :: (Num u, Composite a, Vertical a, Blank a, u ~ PUnit a) + => u -> a -> [a] -> a+vsep n = foldl' (vspace n)+++--------------------------------------------------------------------------------+-- Aligning pictures+++vecMove :: (Composite a, Move a, u ~ PUnit a) => a -> a -> (Vec2 u) -> a +vecMove a b (V2 x y) = a `over` (move x y) b ++alignH :: ( Fractional u, Composite a, Horizontal a, Vertical a, Move a+ , u ~ PUnit a ) + => HAlign -> a -> a -> a+alignH HTop p1 p2 = vecMove p1 p2 (topright p1 .-. topleft p2)+alignH HCenter p1 p2 = vecMove p1 p2 (rightmid p1 .-. leftmid p2)+alignH HBottom p1 p2 = vecMove p1 p2 (bottomright p1 .-. bottomleft p2)++alignV :: ( Fractional u, Composite a, Horizontal a, Vertical a, Move a+ , u ~ PUnit a ) + => VAlign -> a -> a -> a+alignV VLeft p1 p2 = vecMove p1 p2 (bottomleft p1 .-. topleft p2)+alignV VCenter p1 p2 = vecMove p1 p2 (bottommid p1 .-. topmid p2)+alignV VRight p1 p2 = vecMove p1 p2 (bottomright p1 .-. topright p2)+++hcatA :: ( Fractional u, Horizontal a, Vertical a+ , Composite a, Move a, u ~ PUnit a)+ => HAlign -> a -> [a] -> a+hcatA ha = foldl' (alignH ha)++vcatA :: ( Fractional u, Horizontal a, Vertical a+ , Composite a, Move a, u ~ PUnit a)+ => VAlign -> a -> [a] -> a+vcatA va = foldl' (alignV va)++++hsepA :: ( Fractional u, Horizontal a, Vertical a+ , Composite a, Move a, Blank a, u ~ PUnit a)+ => HAlign -> u -> a -> [a] -> a+hsepA ha n = foldl' op where + a `op` b = alignH ha (alignH ha a (blankH n)) b ++vsepA :: ( Fractional u, Horizontal a, Vertical a+ , Composite a, Move a, Blank a, u ~ PUnit a)+ => VAlign -> u -> a -> [a] -> a+vsepA va n = foldl' op where + a `op` b = alignV va (alignV va a (blankV n)) b +
+ src/Wumpus/Core/PostScript.hs view
@@ -0,0 +1,525 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.PostScript+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD3+--+-- Maintainer : Stephen Tetley <stephen.tetley@gmail.com>+-- Stability : highly unstable+-- Portability : GHC+--+-- Wumpus - Writer Monad PostScript +--+-- PostScript is emitted line by line - there is no abstract+-- syntax tree representing PostScript. So we use a writer +-- monad.+---+--------------------------------------------------------------------------------+++module Wumpus.Core.PostScript + (+ -- * Types+ PostScript+ , WumpusM++ , runWumpus++ -- * Deltas + , deltaFontAttr+ , deltaRgbColour++ , deltaStrokeWidth+ , deltaMiterLimit+ , deltaLineCap+ , deltaLineJoin+ , deltaDashPattern+ + -- * Emit PostScript + , ps_comment+ + , ps_gsave+ , ps_grestore+ , ps_setlinewidth+ , ps_setlinecap+ , ps_setlinejoin+ , ps_setmiterlimit+ , ps_setdash+ , ps_setgray+ , ps_setrgbcolor+ , ps_sethsbcolor+ , ps_translate+ , ps_scale+ , ps_concat+ , ps_newpath+ , ps_moveto+ , ps_rmoveto+ , ps_lineto+ , ps_rlineto+ , ps_arc+ , ps_arcn+ , ps_curveto+ , ps_closepath+ , ps_clip+ , ps_fill+ , ps_stroke+ , ps_showpage+ , ps_findfont+ , ps_scalefont+ , ps_setfont+ , ps_show+ , ps_glyphshow+ , bang_PS+ , bang_EPS+ , dsc_comment+ , dsc_BoundingBox+ , dsc_CreationDate+ , dsc_Pages+ , dsc_Page+ , dsc_EndComments+ , dsc_EOF++ ) where++import Wumpus.Core.Colour+import Wumpus.Core.GraphicsState+import Wumpus.Core.TextEncoding+import Wumpus.Core.Utils ( PSUnit(..), roundup, parens, hsep )++import Data.Aviary++import qualified Data.DList as DL+import MonadLib++import Data.List ( foldl' )+++-- Graphics state for PostScript Rendering+--+-- Values with no default value (e.g. font) in the graphics +-- state of the PostScript interpreter (not Wumpus\'s renderer) +-- are Maybes.+-- +-- The graphics state is considered successive - all elements +-- have a colour and all text labels have a font. So during +-- processing there are two situations: ++-- (1) If font or colour is the same as the last no state+-- change needs to be printed.+--+-- (2) If the font or colour changes the update needs to be +-- printed, but as the next element always has a colour (and a+-- font if it is a label), no @undo@ needs to be printed.+--+-- This contrasts with the behaviour for stroke attributes+-- which needs @undo@.++data PostScriptGS = PostScriptGS { + gs_font :: Maybe FontAttr,+ gs_rgb_colour :: DRGB+ }+ deriving (Eq,Show)++++-- | Stroke properties do not have to be fully specified in +-- Wumpus\'s picture types - i.e. a Path might have it\'s stroke +-- width set but nothing else.+--+-- If a path changes any of the stroke properties, it +-- immediately undoes the changes after drawing, returning the +-- the stroke values to their PostScript defaults.+--+-- This means Wumpus doesn't have to carry a nested environment +-- around as it renders to PostScript. As stroke properties can+-- only be assigned to leaves in the picture tree, a nested +-- environment wouldn\'t really be an ideal fit anyway.+++gs_stroke_width :: Double+gs_stroke_width = 1.0++gs_miter_limit :: Double+gs_miter_limit = 10.0++gs_line_cap :: LineCap+gs_line_cap = CapSquare++gs_line_join :: LineJoin+gs_line_join = JoinMiter++gs_dash_pattern :: DashPattern+gs_dash_pattern = Solid++++type PostScript = String++type PsOutput = DL.DList Char++type WumpusM a = PsT Id a+++newtype PsT m a = PsT { + unPsT :: StateT PostScriptGS + (WriterT PsOutput (ReaderT TextEncoder m)) a }++gs_init :: PostScriptGS +gs_init = PostScriptGS { gs_font = Nothing+ , gs_rgb_colour = black + }+ + +runPsT :: Monad m + => TextEncoder -> PsT m a -> m ((a,PostScriptGS),PsOutput)+runPsT i m = runReaderT i $ runWriterT $ runStateT gs_init $ unPsT m++instance Monad m => Functor (PsT m) where+ fmap f (PsT mf) = PsT $ fmap f mf ++instance Monad m => Monad (PsT m) where+ return a = PsT $ return a+ ma >>= f = PsT $ unPsT ma >>= unPsT . f++instance Monad m => WriterM (PsT m) PsOutput where+ put = PsT . put++instance Monad m => ReaderM (PsT m) TextEncoder where+ ask = PsT $ ask++instance Monad m => StateM (PsT m) PostScriptGS where+ set = PsT . set+ get = PsT $ get++instance MonadT PsT where+ lift = PsT . lift . lift . lift+++pstId :: TextEncoder -> PsT Id a -> ((a,PostScriptGS),PsOutput)+pstId = runId `oo` runPsT++-- | Drop state and result, take the Writer trace.+runWumpus :: TextEncoder -> WumpusM a -> String+runWumpus = (DL.toList . snd) `oo` pstId++--------------------------------------------------------------------------------+-- "Deltas" of the graphics state++deltaFontAttr :: FontAttr -> WumpusM (Maybe FontAttr)+deltaFontAttr new = get >>= maybe update diff . gs_font+ where+ update :: WumpusM (Maybe FontAttr)+ update = sets_ (\s -> s { gs_font = Just new }) >> return (Just new)+ + diff :: FontAttr -> WumpusM (Maybe FontAttr)+ diff old | old == new = return Nothing+ | otherwise = update+++deltaRgbColour :: DRGB -> WumpusM (Maybe DRGB)+deltaRgbColour new = get >>= diff . gs_rgb_colour+ where+ diff :: DRGB -> WumpusM (Maybe DRGB)+ diff old | old == new = return Nothing+ | otherwise = do { sets_ (\s -> s { gs_rgb_colour = new })+ ; return (Just new)+ }+++deltaStrokeWidth :: Double -> Maybe (Double,Double)+deltaStrokeWidth n+ | n == gs_stroke_width = Nothing+ | otherwise = Just (n,gs_stroke_width)++deltaMiterLimit :: Double -> Maybe (Double,Double)+deltaMiterLimit n + | n == gs_miter_limit = Nothing+ | otherwise = Just (n,gs_miter_limit)+++deltaLineCap :: LineCap -> Maybe (LineCap,LineCap)+deltaLineCap lc+ | lc == gs_line_cap = Nothing+ | otherwise = Just (lc,gs_line_cap)++deltaLineJoin :: LineJoin -> Maybe (LineJoin,LineJoin)+deltaLineJoin lj + | lj == gs_line_join = Nothing+ | otherwise = Just (lj,gs_line_join)++deltaDashPattern :: DashPattern -> Maybe (DashPattern,DashPattern)+deltaDashPattern p + | p == gs_dash_pattern = Nothing+ | otherwise = Just (p,gs_dash_pattern)++++--------------------------------------------------------------------------------+-- writer monad helpers++tell :: WriterM m i => i -> m ()+tell s = puts ((),s)++writeChar :: WriterM m PsOutput => Char -> m ()+writeChar = tell . DL.singleton +++write :: WriterM m PsOutput => String -> m ()+write = tell . DL.fromList +++writeln :: WriterM m PsOutput => String -> m ()+writeln s = write s >> writeChar '\n'+++writeArg :: WriterM m PsOutput => String -> m () +writeArg s = write s >> writeChar ' '+++++type Command = String++command :: Command -> [String] -> WumpusM ()+command cmd xs = mapM_ writeArg xs >> writeln cmd++++showArray :: (a -> ShowS) -> [a] -> String+showArray _ [] = "[ ]"+showArray f (x:xs) = sfun "]" + where + sfun = foldl' (\a e -> a . (' ':) . f e) (('[':) . f x) xs+ +++-- | @ %% ... @+ps_comment :: String -> WumpusM ()+ps_comment s = write "%% " >> writeln s++--------------------------------------------------------------------------------+-- graphics state operators++-- | @ gsave @+ps_gsave :: WumpusM ()+ps_gsave = command "gsave" []++-- | @ grestore @+ps_grestore :: WumpusM () +ps_grestore = command "grestore" []++-- | @ ... setlinewidth @+ps_setlinewidth :: PSUnit u => u -> WumpusM ()+ps_setlinewidth = command "setlinewidth" . return . dtrunc++-- | @ ... setlinecap @+ps_setlinecap :: LineCap -> WumpusM ()+ps_setlinecap = command "setlinecap" . return . show . fromEnum++-- | @ ... setlinejoin @+ps_setlinejoin :: LineJoin -> WumpusM ()+ps_setlinejoin = command "setlinejoin" . return . show . fromEnum++-- | @ ... setmiterlimit @+ps_setmiterlimit :: PSUnit u => u -> WumpusM ()+ps_setmiterlimit = command "setmiterlimit" . return . dtrunc++-- | @ [... ...] ... setdash @+ps_setdash :: DashPattern -> WumpusM ()+ps_setdash Solid = command "setdash" ["[]", "0"]+ps_setdash (Dash n arr) = command "setdash" [showArray shows arr, show n]++-- | @ ... setgray @+ps_setgray :: PSUnit u => u -> WumpusM ()+ps_setgray = command "setgray" . return . dtrunc ++-- | @ ... ... ... setrgbcolor @+ps_setrgbcolor :: PSUnit u => u -> u -> u -> WumpusM ()+ps_setrgbcolor r g b = command "setrgbcolor" $ map dtrunc [r,g,b]++-- | @ ... ... ... sethsbcolor @+ps_sethsbcolor :: PSUnit u => u -> u -> u -> WumpusM ()+ps_sethsbcolor h s b = command "sethsbcolor" $ map dtrunc [h,s,b]+++--------------------------------------------------------------------------------+-- coordinate system and matrix operators ++-- | @ ... ... translate @+ps_translate :: PSUnit u => u -> u -> WumpusM ()+ps_translate tx ty = do+ command "translate" $ map dtrunc [tx,ty]++-- | @ ... ... scale @+ps_scale :: PSUnit u => u -> u -> WumpusM ()+ps_scale tx ty = do+ command "scale" $ map dtrunc [tx,ty]+++-- Do not use setmatrix for changing the CTM use concat...++-- | @ [... ... ... ... ... ...] concat @+ps_concat :: PSUnit u => CTM u -> WumpusM ()+ps_concat (CTM a b c d e f) = command "concat" [mat] where + mat = showArray ((++) . dtrunc) [a,b,c,d,e,f]+++--------------------------------------------------------------------------------+-- Path construction operators++-- | @ newpath @+ps_newpath :: WumpusM ()+ps_newpath = command "newpath" []+++-- Note - it is preferable to show doubles as 0.0 rather than 0.+-- In PostScript the coercion from int to float is apparently +-- quite expensive.++-- | @ ... ... moveto @+ps_moveto :: PSUnit u => u -> u -> WumpusM ()+ps_moveto x y = command "moveto" [dtrunc x, dtrunc y]++-- | @ ... ... rmoveto @+ps_rmoveto :: PSUnit u => u -> u -> WumpusM ()+ps_rmoveto x y = command "rmoveto" [dtrunc x, dtrunc y]++-- | @ ... ... lineto @+ps_lineto :: PSUnit u => u -> u -> WumpusM ()+ps_lineto x y = command "lineto" [dtrunc x, dtrunc y]++-- | @ ... ... rlineto @+ps_rlineto :: PSUnit u => u -> u -> WumpusM ()+ps_rlineto x y = command "rlineto" [dtrunc x, dtrunc y]++-- | @ ... ... ... ... ... arc @+ps_arc :: PSUnit u => u -> u -> u -> u -> u -> WumpusM ()+ps_arc x y r ang1 ang2 = + command "arc" $ map dtrunc [x,y,r,ang1,ang2]++-- | @ ... ... ... ... ... arcn @+ps_arcn :: PSUnit u => u -> u -> u -> u -> u -> WumpusM ()+ps_arcn x y r ang1 ang2 = + command "arcn" $ map dtrunc [x,y,r,ang1,ang2]++-- | @ ... ... ... ... ... ... curveto @+ps_curveto :: PSUnit u => u -> u -> u -> u -> u -> u -> WumpusM ()+ps_curveto x1 y1 x2 y2 x3 y3 = + command "curveto" $ map dtrunc [x1,y1, x2,y2, x3,y3]++-- | @ closepath @+ps_closepath :: WumpusM ()+ps_closepath = command "closepath" []++-- | @ clip @+ps_clip :: WumpusM ()+ps_clip = command "clip" []++--------------------------------------------------------------------------------+-- painting operators++-- | @ fill @+ps_fill :: WumpusM ()+ps_fill = command "fill" []++-- | @ stroke @+ps_stroke :: WumpusM ()+ps_stroke = command "stroke" []+++--------------------------------------------------------------------------------+-- Output operators++-- | @ showpage @+ps_showpage :: WumpusM ()+ps_showpage = command "showpage" []++++--------------------------------------------------------------------------------+-- Character and font operators++-- | The following fonts are expected to exist on most platforms:+--+-- > Times-Roman Times-Italic Times-Bold Times-BoldItalic+-- > Helvetica Helvetica-Oblique Helvetica-Bold Helvetica-Bold-Oblique+-- > Courier Courier-Oblique Courier-Bold Courier-Bold-Oblique+-- > Symbol+--+-- List from Bill Casselman \'Mathematical Illustrations\' p279.++-- | @ /... findfont @+ps_findfont :: String -> WumpusM () +ps_findfont = command "findfont" . return . ('/' :)++-- | @ ... scalefont @+ps_scalefont :: Int -> WumpusM ()+ps_scalefont = command "scalefont" . return . show++-- | @ setfont @+ps_setfont :: WumpusM ()+ps_setfont = command "setfont" []++-- | @ (...) show @+ps_show :: String -> WumpusM ()+ps_show = command "show" . return . parens++-- | @ (...) show @+ps_glyphshow :: String -> WumpusM ()+ps_glyphshow = command "glyphshow" . return . ('/':)+++--------------------------------------------------------------------------------+-- document structuring conventions++-- | @ %!PS-Adobe-3.0 @+bang_PS :: WumpusM ()+bang_PS = writeln "%!PS-Adobe-3.0"++-- | @ %!PS-Adobe-3.0 EPSF-3.0 @+bang_EPS :: WumpusM ()+bang_EPS = writeln "%!PS-Adobe-3.0 EPSF-3.0"++-- | @ %%...: ... @+dsc_comment :: String -> [String] -> WumpusM ()+dsc_comment name [] = write "%%" >> writeln name+dsc_comment name xs = write "%%" >> write name >> write ": " >> writeln (hsep xs)+++-- | @ %%BoundingBox: ... ... ... ... @ /llx lly urx ury/+dsc_BoundingBox :: PSUnit u => u -> u -> u -> u -> WumpusM ()+dsc_BoundingBox llx lly urx ury = + dsc_comment "BoundingBox" (map (roundup . toDouble) [llx,lly,urx,ury])++-- | @ %%CreationDate: ... @+-- +-- The creation date is informational and never interpreted, +-- thus the format is entirely arbitrary.+dsc_CreationDate :: String -> WumpusM ()+dsc_CreationDate = dsc_comment "CreationDate" . return++-- | @ %%Pages: ... @+dsc_Pages :: Int -> WumpusM ()+dsc_Pages = dsc_comment "Pages" . return . show+++-- | @ %%Page: ... ... @+dsc_Page :: String -> Int -> WumpusM ()+dsc_Page label ordinal = + dsc_comment "Page" [label, show ordinal]+++-- | @ %%EndComments @+dsc_EndComments :: WumpusM ()+dsc_EndComments = dsc_comment "EndComments" []++-- | @ %%EOF @+dsc_EOF :: WumpusM ()+dsc_EOF = dsc_comment "EOF" []+
+ src/Wumpus/Core/SVG.hs view
@@ -0,0 +1,434 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.SVG+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- SVG is represented using XML.Light. XML.Light is a simple,+-- generic XML representation (almost) everything is an element +-- with attributes.+--+-- SVG output is monadic to handle clipping paths and +-- configurable text encoding via a Reader monad. +--+-- SVG does not achieve clipping by changing the graphics state +-- (being /declarative/ SVG doesn\'t have a graphics state as +-- such). Instead a clipping path has an id, subsequent elements +-- that are bound by the clipping path are tagged with a +-- @clip-path@ attribute that references the clipping path id: +--+-- > clip-path=\"url(#clip1)\"+-- +-- +-- The operations to build XML elements (e.g. element_path) don\'t +-- take more parameters than necessary, and are expected to be +-- augmented with attributes using 'add_attr' and 'add_attrs' from +-- the XML.Light library.+-- +--------------------------------------------------------------------------------++module Wumpus.Core.SVG + (+ -- * SVG Monad + SvgM+ , runSVG+ , newClipLabel+ , currentClipLabel +++ -- * Build SVG+ , SvgPath++ , unqualAttr+ , xmlVersion+ , svgDocType+ , gElement+ , svgElement+ + , element_circle+ , element_ellipse+ , attr_x+ , attr_y+ , attr_r+ , attr_rx+ , attr_ry+ , attr_cx+ , attr_cy+ , element_path+ , element_clippath+ , element_text+ , element_tspan+ , content_text+ , attr_font_family+ , attr_font_size+ , attr_font_weight+ , attr_font_style+ , attr_id+ , attr_fill+ , attr_fill_none+ , attr_stroke+ , attr_stroke_none+ , attr_stroke_width+ , attr_stroke_miterlimit+ , attr_stroke_linecap+ , attr_stroke_linejoin++ , attr_stroke_dasharray+ , attr_stroke_dasharray_none+ , attr_stroke_dashoffset++ , attr_color+ , attr_clippath+ , attr_transform+ , val_matrix+ , val_colour+ , val_rgb+ , val_url+ , val_translate+ , path_m+ , path_l+ , path_s+++ ) where++import Wumpus.Core.Colour+import Wumpus.Core.GraphicsState+import Wumpus.Core.TextEncoding+import Wumpus.Core.Utils++import Data.Aviary++import MonadLib hiding ( version )+import Text.XML.Light+++data SvgState = SvgSt { clipCount :: Int }++-- | The SVG monad - which wraps a state monad to generate +-- fresh names.+type SvgM a = SvgT Id a++newtype SvgT m a = SvgT { unSvgT :: StateT SvgState (ReaderT TextEncoder m) a }++runSvgT :: Monad m => TextEncoder -> SvgT m a -> m (a,SvgState)+runSvgT i m = runReaderT i $ runStateT st0 $ unSvgT m where+ st0 = SvgSt { clipCount = 0 } ++instance Monad m => Functor (SvgT m) where+ fmap f (SvgT mf) = SvgT $ fmap f mf ++instance Monad m => Monad (SvgT m) where+ return a = SvgT $ return a+ ma >>= f = SvgT $ unSvgT ma >>= unSvgT . f++instance Monad m => StateM (SvgT m) SvgState where+ get = SvgT $ get+ set = SvgT . set++instance Monad m => ReaderM (SvgT m) TextEncoder where+ ask = SvgT $ ask++instance MonadT SvgT where+ lift = SvgT . lift . lift+++svgId :: TextEncoder -> SvgT Id a -> (a,SvgState)+svgId = runId `oo` runSvgT ++-- | Run the SVG monad.+runSVG :: TextEncoder -> SvgM a -> a+runSVG = fst `oo` svgId+++-- | Get the current clip label.+currentClipLabel :: SvgM String+currentClipLabel = get >>= return . clipname . clipCount++-- | Generate a new clip label.+newClipLabel :: SvgM String+newClipLabel = do + i <- (get >>= return . clipCount)+ sets_ (\s -> s { clipCount=i+1 })+ return $ clipname i+++clipname :: Int -> String+clipname = ("clip" ++) . show+++--------------------------------------------------------------------------------+-- Helpers for XML.Light and /data in strings/.++-- | Helper for XML.Light+unqualAttr :: String -> String -> Attr+unqualAttr name val = Attr (unqual name) val+++--------------------------------------------------------------------------------+-- SVG helpers++type SvgPath = [String]+++-- | @ \<?xml version=\"1.0\" encoding=\"...\"?\> @+--+xmlVersion :: String -> CData+xmlVersion s = CData CDataRaw + ("<?xml version=\"1.0\" encoding=\"" ++ s ++ "\"?>")+ (Just 1)++-- |+-- > <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" +-- > "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +--+svgDocType :: CData+svgDocType = CData CDataRaw (line1 ++ "\n" ++ line2) (Just 1)+ where+ line1 = "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\""+ line2 = " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"++-- | +-- > <g> ... </g>+--+-- Wumpus uses the g element (group) to achieve nesting. +gElement :: [Attr] -> [Element] -> Element+gElement xs ys = unode "g" (xs,ys)++-- |+-- > <svg xmlns="http://www.w3.org/2000/svg" version="1.1">+-- > ...+-- > </svg>+--+svgElement :: [Element] -> Element+svgElement xs = unode "svg" ([xmlns,version],xs)+ where+ xmlns = unqualAttr "xmlns" "http://www.w3.org/2000/svg"+ version = unqualAttr "version" "1.1" +++--------------------------------------------------------------------------------++++-- |+-- > <circle/>+--+element_circle :: Element+element_circle = unode "circle" ()++-- |+-- > <ellipse/>+--+element_ellipse :: Element+element_ellipse = unode "ellipse" ()++++-- | @ x=\"...\" @+attr_x :: PSUnit u => u -> Attr+attr_x = unqualAttr "x" . dtrunc++-- | @ y=\"...\" @+attr_y :: PSUnit u => u -> Attr+attr_y = unqualAttr "y" . dtrunc++-- | @ r=\"...\" @+attr_r :: PSUnit u => u -> Attr+attr_r = unqualAttr "r" . dtrunc+++-- | @ rx=\"...\" @+attr_rx :: PSUnit u => u -> Attr+attr_rx = unqualAttr "rx" . dtrunc++-- | @ ry=\"...\" @+attr_ry :: PSUnit u => u -> Attr+attr_ry = unqualAttr "ry" . dtrunc++-- | @ cx=\"...\" @+attr_cx :: PSUnit u => u -> Attr+attr_cx = unqualAttr "cx" . dtrunc++-- | @ cy=\"...\" @+attr_cy :: PSUnit u => u -> Attr+attr_cy = unqualAttr "cy" . dtrunc+++++-- |+-- > <path d="..." />+--+-- Note the argument to this function is an attribute rather+-- than content. We have no use for empty paths.+element_path :: SvgPath -> Element+element_path = unode "path" . attr_d++-- |+-- > <clipPath>+-- > ...+-- > </clipPath>+--+element_clippath :: SvgPath -> Element+element_clippath = unode "clipPath" . element_path++-- |+-- > <text>...</text>+--+element_text :: Node t => t -> Element+element_text = unode "text" ++-- |+-- > <text>...</text>+--+element_tspan :: String -> Element+element_tspan = unode "tspan" . content_text+++-- | Render the string as 'CDataText' - see XML.Light.+content_text :: String -> Content+content_text str = Text $ CData CDataRaw str Nothing+++-- | @ font-family=\"...\" @+attr_font_family :: String -> Attr+attr_font_family = unqualAttr "font-family" ++-- | @ font-size=\"...\" @+attr_font_size :: Int -> Attr+attr_font_size = unqualAttr "font-size" . show++-- | @ font-weight=\"...\" @+attr_font_weight :: String -> Attr+attr_font_weight = unqualAttr "font-weight"++-- | @ font-style=\"...\" @+attr_font_style :: String -> Attr+attr_font_style = unqualAttr "font-style"+++-- | @ id=\"...\" @+attr_id :: String -> Attr+attr_id = unqualAttr "id" ++-- | @ d="..." @+attr_d :: SvgPath -> Attr+attr_d = unqualAttr "d" . hsep++-- | @ fill=\"rgb(..., ..., ...)\" @+attr_fill :: PSColour c => c -> Attr+attr_fill = unqualAttr "fill" . val_colour++-- | @ fill=\"none\" @+attr_fill_none :: Attr+attr_fill_none = unqualAttr "fill" "none"++-- | @ stroke=\"rgb(..., ..., ...)\" @+attr_stroke :: PSColour c => c -> Attr+attr_stroke = unqualAttr "stroke" . val_colour++-- | @ stroke=\"none\" @+attr_stroke_none :: Attr+attr_stroke_none = unqualAttr "stroke" "none"++-- | @ stroke-width=\"...\" @+attr_stroke_width :: PSUnit u => u -> Attr+attr_stroke_width = unqualAttr "stoke-width" . dtrunc+++-- | @ stroke-miterlimit=\"...\" @+attr_stroke_miterlimit :: PSUnit u => u -> Attr+attr_stroke_miterlimit = unqualAttr "stoke-miterlimit" . dtrunc++-- | @ stroke-linejoin=\"...\" @+attr_stroke_linejoin :: LineJoin -> Attr+attr_stroke_linejoin JoinMiter = unqualAttr "stroke-linejoin" "miter"+attr_stroke_linejoin JoinRound = unqualAttr "stroke-linejoin" "round"+attr_stroke_linejoin JoinBevel = unqualAttr "stroke-linejoin" "bevel"++++attr_stroke_linecap :: LineCap -> Attr+attr_stroke_linecap CapButt = unqualAttr "stroke-linecap" "butt"+attr_stroke_linecap CapRound = unqualAttr "stroke-linecap" "round"+attr_stroke_linecap CapSquare = unqualAttr "stroke-linecap" "square"+++-- | @ stroke-dasharray=\"...\" @+attr_stroke_dasharray :: [Int] -> Attr+attr_stroke_dasharray = unqualAttr "stroke-dasharray" . commasep . map show++-- | @ stroke-dasharray=\"none\" @+attr_stroke_dasharray_none :: Attr+attr_stroke_dasharray_none = unqualAttr "stroke-dasharray" "none"++-- | @ stroke-dashoffset=\"...\" @+attr_stroke_dashoffset :: Int -> Attr+attr_stroke_dashoffset = unqualAttr "stroke-dashoffset" . show++-- | @ color=\"rgb(..., ..., ...)\" @+--+-- Gray or HSB values will be converted to and rendered as RGB.+attr_color :: PSColour c => c -> Attr+attr_color = unqualAttr "color" . val_colour++-- | @ clip-path=\"url(#...)\" @+attr_clippath :: String -> Attr+attr_clippath = unqualAttr "clip-path" . val_url++-- | @ transform="..." @+attr_transform :: String -> Attr+attr_transform = unqualAttr "transform"++-- | @ matrix(..., ..., ..., ..., ..., ...) @+val_matrix :: PSUnit u => u -> u -> u -> u -> u -> u -> String+val_matrix a b c d e f = "matrix" ++ tupled (map dtrunc [a,b,c,d,e,f])++++-- | @ rgb(..., ..., ...) @+-- +-- HSB and gray scale are translated to RGB values.+val_colour :: PSColour c => c -> String+val_colour = val_rgb . psColour+++-- | @ rgb(..., ..., ...) @+val_rgb :: RGB3 Double -> String+val_rgb (RGB3 r g b) = "rgb" ++ show (ramp255 r,ramp255 g,ramp255 b)+++-- | @ url(#...) @+val_url :: String -> String+val_url s = "url" ++ parens ('#':s)++-- | @ translate(..., ...) @+val_translate :: PSUnit u => u -> u -> String+val_translate x y = "translate" ++ tupled (map dtrunc [x,y])+ +-- | @ M ... ... @+--+-- c.f. PostScript's @moveto@.+path_m :: PSUnit u => u -> u -> String+path_m x y = hsep $ "M" : map dtrunc [x,y]++-- | @ L ... ... @+--+-- c.f. PostScript's @lineto@.+path_l :: PSUnit u => u -> u -> String+path_l x y = hsep $ "L" : map dtrunc [x,y]++-- | @ S ... ... ... ... ... ... @+-- +-- c.f. PostScript's @curveto@.+path_s :: PSUnit u => u -> u -> u -> u -> u -> u -> String+path_s x1 y1 x2 y2 x3 y3 = hsep $ "S" : map dtrunc [x1,y1,x2,y2,x3,y3]++
+ src/Wumpus/Core/TextEncoding.hs view
@@ -0,0 +1,119 @@+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.TextEncoding+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Extended character handling...+-- +--------------------------------------------------------------------------------++module Wumpus.Core.TextEncoding+ ( + GlyphName+ , CharCode+ , PostScriptLookup+ , SVGLookup+ , TextEncoder(..)++ , EncodedText(..) + , TextChunk(..)++ , textLength+ , lookupByCharCode + , lookupByGlyphName++ , lexLabel++ ) where++import Text.PrettyPrint.Leijen hiding ( SText )++import Data.Char++type GlyphName = String+type CharCode = Int ++type PostScriptLookup = CharCode -> Maybe GlyphName+type SVGLookup = GlyphName -> Maybe CharCode++data TextEncoder = TextEncoder {+ ps_lookup :: PostScriptLookup,+ svg_lookup :: SVGLookup,+ svg_encoding_name :: String,+ ps_fallback :: GlyphName,+ svg_fallback :: CharCode+ }+ +++newtype EncodedText = EncodedText { getEncodedText :: [TextChunk] }+ deriving (Eq,Show)+++data TextChunk = SText String+ | EscInt Int+ | EscStr GlyphName+ deriving (Eq,Show)+++--------------------------------------------------------------------------------++instance Pretty EncodedText where+ pretty = hcat . map pretty . getEncodedText++instance Pretty TextChunk where+ pretty (SText s) = string s+ pretty (EscInt i) = text "&#" <> int i <> semi+ pretty (EscStr s) = text "&#" <> text s <> semi++--------------------------------------------------------------------------------++textLength :: EncodedText -> Int+textLength = foldr add 0 . getEncodedText where + add (SText s) n = n + length s+ add _ n = n + 1+++lookupByCharCode :: CharCode -> TextEncoder -> Maybe GlyphName+lookupByCharCode i enc = (ps_lookup enc) i++lookupByGlyphName :: GlyphName -> TextEncoder -> Maybe CharCode+lookupByGlyphName i enc = (svg_lookup enc) i+++-- | Output to PostScript as @ /egrave glyphshow @++-- Output to SVG as an escaped decimal, e.g. @ è @+--+-- Note, HTML entity names do not seem to be supported in SVG,+-- @ è @ does not work in FireFox or Chrome.+++lexLabel :: String -> EncodedText+lexLabel = EncodedText . lexer++lexer :: String -> [TextChunk]+lexer [] = []++lexer ('&':'#':xs) = esc xs+ where+ esc (c:cs) | isDigit c = let (s,cs') = span isDigit cs + in intval (c:s) cs'+ | otherwise = let (s,cs') = span isAlpha cs + in EscStr (c:s) : optsemi cs'+ esc [] = []++ optsemi (';':cs) = lexer cs -- let ill-formed go through+ optsemi cs = lexer cs++ intval [] rest = optsemi rest+ intval cs rest = EscInt (read cs) : optsemi rest++lexer (x:xs) = let (s,xs') = span (/= '&') xs + in SText (x:s) : lexer xs'
+ src/Wumpus/Core/TextLatin1.hs view
@@ -0,0 +1,250 @@+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.TextLatin1+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD-style (see LICENSE)+-- Maintainer : stephen.tetley@gmail.com+-- Stability : experimental+-- Portability : GHC only+--+-- Extended character handling...+-- +--------------------------------------------------------------------------------++module Wumpus.Core.TextLatin1+ ( + Latin1Name+ , Latin1ISOCode+ + , latin1Encoder++ , latin1All++ ) where++import Wumpus.Core.TextEncoding++import qualified Data.Map as Map++type Latin1Name = String+type Latin1ISOCode = Int++latin1Encoder :: TextEncoder+latin1Encoder = TextEncoder {+ ps_lookup = Map.lookup `flip` codeToName,+ svg_lookup = Map.lookup `flip` nameToCode,+ svg_encoding_name = "ISO-8859-1",+ ps_fallback = "space",+ svg_fallback = 0o040+ }+nameToCode :: Map.Map String Int+nameToCode = Map.fromList latin1All++codeToName :: Map.Map Int String+codeToName = foldr fn Map.empty latin1All where+ fn (s,i) a = Map.insert i s a ++latin1All :: [(Latin1Name, Latin1ISOCode)]+latin1All = [ ("A", 0o101)+ , ("AE", 0o306)+ , ("Aacute", 0o301)+ , ("Acircumflex", 0o302)+ , ("Adieresis", 0o304)+ , ("Agrave", 0o300)+ , ("Aring", 0o305)+ , ("Atilde", 0o303)+ , ("B", 0o102)+ , ("C", 0o103)+ , ("Ccedilla", 0o307)+ , ("D", 0o104)+ , ("E", 0o105)+ , ("Eacute", 0o311)+ , ("Ecircumflex", 0o312)+ , ("Edieresis", 0o313)+ , ("Egrave", 0o310)+ , ("Eth", 0o320)+ , ("F", 0o106)+ , ("G", 0o107)+ , ("H", 0o110)+ , ("I", 0o111)+ , ("Iacute", 0o315)+ , ("Icircumflex", 0o316)+ , ("Idieresis", 0o317)+ , ("Igrave", 0o314)+ , ("J", 0o112)+ , ("K", 0o113)+ , ("L", 0o114)+ , ("M", 0o115)+ , ("N", 0o116)+ , ("Ntilde", 0o321)+ , ("O", 0o117)+ , ("Oacute", 0o323)+ , ("Ocircumflex", 0o324)+ , ("Odieresis", 0o326)+ , ("Ograve", 0o322)+ , ("Oslash", 0o351)+ , ("Otilde", 0o325)+ , ("P", 0o120)+ , ("Q", 0o121)+ , ("R", 0o122)+ , ("S", 0o123)+ , ("T", 0o124)+ , ("Thorn", 0o336)+ , ("U", 0o125)+ , ("Uacute", 0o332)+ , ("Ucircumflex", 0o333)+ , ("Udieresis", 0o334)+ , ("Ugrave", 0o331)+ , ("V", 0o126)+ , ("W", 0o127)+ , ("X", 0o130)+ , ("Y", 0o131)+ , ("Yacute", 0o335)+ , ("Z", 0o132)+ , ("a", 0o141)+ , ("aacute", 0o341)+ , ("acircumflex", 0o342)+ , ("acute2", 0o264)+ , ("adieresis", 0o344)+ , ("ae", 0o346)+ , ("agrave", 0o340)+ , ("ampersand", 0o046)+ , ("aring", 0o345)+ , ("asciicircum", 0o136)+ , ("asciitilde", 0o176)+ , ("asterisk", 0o052)+ , ("at", 0o100)+ , ("atilde", 0o343)+ , ("b", 0o142)+ , ("backslash", 0o134)+ , ("bar", 0o174)+ , ("braceleft", 0o173)+ , ("braceright", 0o175)+ , ("bracketleft", 0o133)+ , ("bracketright", 0o135)+ , ("breve", 0o226)+ , ("brokenbar", 0o246)+ , ("c", 0o143)+ , ("caron", 0o237)+ , ("ccedilla", 0o347)+ , ("cedilla", 0o270)+ , ("cent", 0o242)+ , ("circumflex", 0o223)+ , ("colon", 0o072)+ , ("comma", 0o054)+ , ("copyright", 0o251)+ , ("currency", 0o244)+ , ("d", 0o144)+ , ("degree", 0o260)+ , ("dieresis", 0o250)+ , ("divide", 0o367)+ , ("dollar", 0o044)+ , ("dotaccent", 0o227)+ , ("dotlessi", 0o220)+ , ("e", 0o145)+ , ("eacute", 0o351)+ , ("ecircumflex", 0o352)+ , ("edieresis", 0o353)+ , ("egrave", 0o350)+ , ("eight", 0o070)+ , ("equal", 0o075)+ , ("eth", 0o360)+ , ("exclam", 0o041)+ , ("exclamdown", 0o241)+ , ("f", 0o146)+ , ("five", 0o065)+ , ("four", 0o064)+ , ("g", 0o147)+ , ("germandbls", 0o337)+ , ("grave", 0o221)+ , ("greater", 0o076)+ , ("guillemotleft", 0o253)+ , ("guillemotright", 0o273)+ , ("h", 0o150)+ , ("hungarumlaut", 0o235)+ , ("hyphen", 0o255)+ , ("i", 0o151)+ , ("iacute", 0o355)+ , ("icircumflex", 0o356)+ , ("idieresis", 0o357)+ , ("igrave", 0o354)+ , ("j", 0o152)+ , ("k", 0o153)+ , ("l", 0o154)+ , ("less", 0o074)+ , ("logicalnot", 0o254)+ , ("m", 0o155)+ , ("macron", 0o257)+ , ("minus", 0o055)+ , ("mu", 0o265)+ , ("multiply", 0o327)+ , ("n", 0o156)+ , ("nine", 0o071)+ , ("ntilde", 0o361)+ , ("numbersign", 0o043)+ , ("o", 0o157)+ , ("oacute", 0o363)+ , ("ocircumflex", 0o364)+ , ("odieresis", 0o366)+ , ("ogonek", 0o236)+ , ("ograve", 0o362)+ , ("one", 0o061)+ , ("onehalf", 0o275)+ , ("onequarter", 0o274)+ , ("onesuperior", 0o271)+ , ("ordfeminine", 0o252)+ , ("ordmasculine", 0o272)+ , ("oslash", 0o370)+ , ("otilde", 0o365)+ , ("p", 0o160)+ , ("paragraph", 0o266)+ , ("parenleft", 0o050)+ , ("parenright", 0o051)+ , ("percent", 0o045)+ , ("period", 0o056)+ , ("periodcentered", 0o267)+ , ("plus", 0o053)+ , ("plusminus", 0o261)+ , ("q", 0o161)+ , ("question", 0o077)+ , ("questiondown", 0o277)+ , ("quotedbl", 0o042)+ , ("quoteleft", 0o140)+ , ("quoteright", 0o047)+ , ("r", 0o162)+ , ("registered", 0o256)+ , ("ring", 0o232)+ , ("s", 0o163)+ , ("section", 0o247)+ , ("semicolon", 0o073)+ , ("seven", 0o067)+ , ("six", 0o066)+ , ("slash", 0o057)+ , ("space", 0o040)+ , ("sterling", 0o243)+ , ("t", 0o164)+ , ("thorn", 0o376)+ , ("three", 0o063)+ , ("threequarters", 0o276)+ , ("threesuperior", 0o263)+ , ("tilde", 0o224)+ , ("two", 0o062)+ , ("twosuperior", 0o262)+ , ("u", 0o165)+ , ("uacute", 0o372)+ , ("ucircumflex", 0o373)+ , ("udieresis", 0o374)+ , ("ugrave", 0o371)+ , ("underscore", 0o137)+ , ("v", 0o166)+ , ("w", 0o167)+ , ("x", 0o170)+ , ("y", 0o171)+ , ("yacute", 0o375)+ , ("ydieresis", 0o377)+ , ("yen", 0o245)+ , ("z", 0o172)+ , ("zero", 0o060)+ ]
+ src/Wumpus/Core/Utils.hs view
@@ -0,0 +1,270 @@+{-# LANGUAGE FlexibleInstances #-}+{-# OPTIONS -Wall #-}++--------------------------------------------------------------------------------+-- |+-- Module : Wumpus.Core.Utils+-- Copyright : (c) Stephen Tetley 2009+-- License : BSD3+--+-- Maintainer : Stephen Tetley <stephen.tetley@gmail.com>+-- Stability : highly unstable+-- Portability : GHC+--+-- Utility functions+--+--------------------------------------------------------------------------------+++module Wumpus.Core.Utils+ ( ++ -- * Component-wise min and max+ CMinMax(..)+ , within++ -- * Three values + , max3+ , min3+ , med3+++ -- * Truncate / print a double+ , PSUnit(..)+ , truncateDouble+ , roundup++ , clamp+ , ramp+ , ramp255++ -- * PostScript timetmap+ , mkTimeStamp++ -- * Pretty printers for strings + , parens+ , hsep+ , commasep+ , tupled++ -- * Extras + , sequenceA+ , (<:>) +++ ++ -- * One type - non-empty list type+ , OneList(..)+ , mkList2+ , onesmapM_+ , toListWith+ , toListWithM+ , fromListErr++ ) where++++import Control.Applicative+import Control.Monad ( ap )+import Data.List ( intersperse )+import Data.Ratio+import System.Time +++++--------------------------------------------------------------------------------++-- | /Component-wise/ min and max. +-- Standard 'min' and 'max' via Ord are defined lexographically+-- on pairs, e.g.:+-- +-- > min (1,2) (2,1) = (1,2)+-- +-- For certain geometrical objects (Points!) we want the +-- (constructed-) componentwise min and max, e.g:+--+-- > cmin (1,2) (2,1) = (1,1) +-- > cmax (1,2) (2,1) = (2,2)+-- ++class CMinMax a where+ cmin :: a -> a -> a+ cmax :: a -> a -> a++++instance (Ord a, Ord b) => CMinMax (a,b) where+ cmin (x,y) (x',y') = (min x x', min y y')+ cmax (x,y) (x',y') = (max x x', max y y')+++-- | Test whether a is within opper and lower.+within :: Eq a => CMinMax a => a -> a -> a -> Bool+within a lower upper = (cmin a lower) == lower && (cmax a upper) == upper+++-- | max of 3+max3 :: Ord a => a -> a -> a -> a+max3 a b c = max (max a b) c++-- | min of 3+min3 :: Ord a => a -> a -> a -> a+min3 a b c = min (min a b) c+++-- | median of 3+med3 :: Ord a => a -> a -> a -> a+med3 a b c = if c <= x then x else if c > y then y else c+ where + (x,y) = order a b+ order p q | p <= q = (p,q)+ | otherwise = (q,p)+++--------------------------------------------------------------------------------+-- PS Unit++class Num a => PSUnit a where+ toDouble :: a -> Double+ dtrunc :: a -> String+ + dtrunc = truncateDouble . toDouble++instance PSUnit Double where+ toDouble = id+ dtrunc = truncateDouble++instance PSUnit Float where+ toDouble = realToFrac++instance PSUnit (Ratio Integer) where+ toDouble = realToFrac++instance PSUnit (Ratio Int) where+ toDouble = realToFrac+++-- | Truncate the printed decimal representation of a Double.+-- The is prefered to 'showFFloat' from Numeric as it produces+-- shorter representations where appropriate.+-- +-- 0.000000000 becomes 0.0 rather than however many digs are +-- specified.+-- +truncateDouble :: Double -> String+truncateDouble d | abs d < 0.0001 = "0.0"+ | d < 0.0 = '-' : show (abs tx)+ | otherwise = show tx+ where+ tx :: Double+ tx = (realToFrac (roundi (d*1000000.0))) / 1000000.0++roundi :: RealFrac a => a -> Integer+roundi = round++-- | Take 'ceilingi' and show.+roundup :: Double -> String+roundup = show . ceilingi++-- Avoid those annoying 'Defaulting ...' warnings...+ceilingi :: RealFrac a => a -> Integer+ceilingi = ceiling+++clamp :: Ord a => a -> a -> a -> a +clamp a b x = max a (min b x)++ramp :: Double -> Double+ramp = clamp 0 1++-- | Scale a Double between 0.0 and 1.0 to be an Int between 0 +-- and 255.+ramp255 :: Double -> Int+ramp255 = clamp 0 255 . ceiling . (*255)++++-- | Generate a time stamp for the output files. Note PostScript+-- does no interpretation of the time stamp, it is solely for +-- information and so the representation is arbitrary.+mkTimeStamp :: IO String+mkTimeStamp = getClockTime >>= toCalendarTime >>= return . format+ where+ format t = mkTime t ++ " " ++ mkDate t+ mkTime = concat . intersperse ":" . sequenceA tfuns+ mkDate = concat . intersperse " " . sequenceA dfuns+ tfuns = [ pad2 . ctHour, pad2 . ctMin, pad2 . ctSec ]+ dfuns = [ show . ctDay, show . ctMonth, show . ctYear ]+ pad2 i | i < 10 = '0' : show i+ | otherwise = show i ++++-- | Enclose string in parens.+parens :: String -> String +parens s = "(" ++ s ++ ")"++-- | Separate with a space.+hsep :: [String] -> String+hsep = concat . intersperse " "++commasep :: [String] -> String+commasep = concat . intersperse ","++-- | @ (..., ...)@+tupled :: [String] -> String+tupled = parens . concat . intersperse ", " ++++-- | Applicative version of (monadic) 'sequence'.+-- Because we use MonadLib we don't want to bring in +-- Control.Monad.Instances ()+sequenceA :: Applicative f => [f a] -> f [a]+sequenceA = foldr (<:>) (pure []) +++-- | Applicative 'cons'.+infixr 6 <:>+(<:>) :: Applicative f => f a -> f [a] -> f [a]+(<:>) a b = (:) <$> a <*> b++++--------------------------------------------------------------------------------++infixr 5 :+++data OneList a = One a | a :+ OneList a+ deriving (Eq)++instance Show a => Show (OneList a) where+ show = ('{':) . ($ []) . step where+ step (One a) = shows a . showChar '}'+ step (a :+ xs) = shows a . showChar ',' . step xs+++mkList2 :: a -> a -> OneList a+mkList2 a b = a :+ One b+++onesmapM_ :: Monad m => (a -> m b) -> OneList a -> m ()+onesmapM_ f (One a) = f a >> return ()+onesmapM_ f (a :+ xs) = f a >> onesmapM_ f xs++toListWith :: (a -> b) -> OneList a -> [b]+toListWith f (One a) = [f a]+toListWith f (a :+ xs) = f a : toListWith f xs++toListWithM :: Monad m => (a -> m b) -> OneList a -> m [b]+toListWithM f (One a) = return return `ap` f a+toListWithM f (a :+ xs) = return (:) `ap` f a `ap` toListWithM f xs+++fromListErr :: String -> [a] -> OneList a+fromListErr msg [] = error msg+fromListErr _ [a] = One a+fromListErr msg (a:xs) = a :+ fromListErr msg xs
+ wumpus-core.cabal view
@@ -0,0 +1,107 @@+name: wumpus-core+version: 0.12.0+license: BSD3+license-file: LICENSE+copyright: Stephen Tetley <stephen.tetley@gmail.com>+maintainer: Stephen Tetley <stephen.tetley@gmail.com>+homepage: http://code.google.com/p/copperbox/+category: Graphics+synopsis: Pure Haskell PostScript and SVG generation. +description:+ Wumpus - (W)riter (M)onad (P)ost (S)cript. + .+ Wumpus is a library for generating 2D vector pictures, its + salient feature is portability due to no FFI dependencies. + It can generate PostScript (EPS) files and SVG files. The + generated PostScript code is quite efficient (no unnecessary + stack operations) and plain [1].+ .+ Pictures in Wumpus are made from /paths/ and text /labels/. + Paths themselves are made from points. The usual affine + transformations (rotations, scaling, translations) can be+ applied to all geometric objects. Unlike PostScript there + is no notion of a current point, Wumpus builds pictures in a+ coordinate-free style. There is a set of combinators for + composing pictures (more-or-less similar to the usual pretty+ printing combinators).+ .+ THE DRAWBACKS...+ .+ For actually drawing pictures, diagrams, etc. Wumpus is very + low level. I am working on a complementary package + @wumpus-extra@ with higher-level stuff (polygons, arrows etc.)+ but it is far too unstable for Hackage. Preview releases can be+ found at http://code.google.com/p/copperbox/ though.+ .+ Wumpus-core should be fairly stable from now on, except for+ the fact that more modules are exposed than strictly necessary+ this is so their Haddock docs are available. Client\'s should + only import the @Wumpus.Core@ module. There may be some name + changes etc. that will change interfaces, but Wumpus has + been carefully implemented. Some of the design decisions are+ not sophisticated (e.g. how attributes like colour are handled, + and the bounding boxes of text labels are calculated), so+ Wumpus might be limited compared to other systems but its + design permits a simple implementation - which is a priority. + Text encoding is the exception, the current implementation + appears reasonable for Latin 1 but I\'m not sure about other + character sets, and I might have to revise it significantly.+ .+ /There is no documentation/ - the graphics model used by + Wumpus is different to PostScript or SVG, and Wumpus really + needs a manual. Unfortunately there isn\'t one yet, and I will + be focusing on @wumpus-extra@ for the foreseeable future so a+ manual won\'t be written soon. If you want FFI-free vector+ graphics and Wumpus seems to otherwise fit the task, please + email me and I will try to help.+ .+ \[1\] Because the output is simple, straight-line PostScript + code it is possible to use GraphicsMagick or similar tools to + convert Wumpus'\s EPS files to many other formats (bitmaps). + +build-type: Simple+stability: highly unstable+cabal-version: >= 1.2++extra-source-files:+ demo/LabelPic.hs,+ demo/Picture.hs+++library+ hs-source-dirs: src+ build-depends: base < 5, containers, old-time,+ wl-pprint, vector-space, + monadLib, xml, dlist, algebra, + data-aviary > 0.1.0+ + exposed-modules:+ Wumpus.Core,+ Wumpus.Core.AffineTrans,+ Wumpus.Core.BoundingBox,+ Wumpus.Core.Colour,+ Wumpus.Core.FontSize,+ Wumpus.Core.Geometry,+ Wumpus.Core.GraphicsState,+ Wumpus.Core.OutputPostScript,+ Wumpus.Core.OutputSVG,+ Wumpus.Core.Picture,+ Wumpus.Core.PictureInternal,+ Wumpus.Core.PictureLanguage,+ Wumpus.Core.PostScript,+ Wumpus.Core.SVG,+ Wumpus.Core.TextEncoding,+ Wumpus.Core.TextLatin1,+ Wumpus.Core.Utils+ other-modules:+ + extensions:+ ++ ghc-options:+ + includes: + ++ +