{-# LANGUAGE FlexibleContexts
, FlexibleInstances
, DeriveDataTypeable
, GeneralizedNewtypeDeriving
, TypeFamilies
#-}{-# OPTIONS_GHC -fno-warn-orphans #-}------------------------------------------------------------------------------- |-- Module : Diagrams.TwoD.Path-- Copyright : (c) 2011 diagrams-lib team (see LICENSE)-- License : BSD-style (see LICENSE)-- Maintainer : diagrams-discuss@googlegroups.com---- Paths in two dimensions are special since we may stroke them to-- create a 2D diagram, and (eventually) perform operations such as-- intersection and union.-------------------------------------------------------------------------------moduleDiagrams.TwoD.Path(-- * Constructing path-based diagramsstroke,stroke',strokeT,strokeT'-- ** Stroke options,FillRule(..),FillRuleA(..),getFillRule,fillRule,StrokeOpts(..)-- ** Inside/outside testing,isInsideWinding,isInsideEvenOdd-- * Clipping,Clip(..),clipBy)whereimportGraphics.Rendering.DiagramsimportMath.TauimportDiagrams.SegmentimportDiagrams.PathimportDiagrams.TwoD.TypesimportDiagrams.SolveimportData.AffineSpaceimportData.VectorSpaceimportData.Semigrouphiding((<>))importControl.Applicative(liftA2)importqualifiedData.FoldableasFimportData.DefaultimportData.Typeable-------------------------------------------------------------- Constructing path-based diagrams ------------------------------------------------------------------------------------ | Convert a path into a diagram. The resulting diagram has the-- names 0, 1, ... assigned to each of the path's vertices.---- See also 'stroke'', which takes an extra options record allowing-- its behavior to be customized.---- Note that a bug in GHC 7.0.1 causes a context stack overflow when-- inferring the type of @stroke@. The solution is to give a type-- signature to expressions involving @stroke@, or (recommended)-- upgrade GHC (the bug is fixed in 7.0.2 onwards).stroke::Renderable(PathR2)b=>PathR2->DiagrambR2stroke=stroke'(def::StrokeOpts())instanceRenderable(PathR2)b=>PathLike(AnnDiagrambR2Any)wherepathLikestclsegs=stroke$pathLikestclsegs-- | A variant of 'stroke' that takes an extra record of options to-- customize its behavior. In particular:---- * Names can be assigned to the path's vertices---- 'StrokeOpts' is an instance of 'Default', so @stroke' 'with' {-- ... }@ syntax may be used.stroke'::(Renderable(PathR2)b,IsNamea)=>StrokeOptsa->PathR2->DiagrambR2stroke'optsp=mkAD(Primp)(getBoundsp)(fromNames.concat$zipWithzip(vertexNamesopts)(pathVerticesp))(Query$Any.flip(runFillRule(queryFillRuleopts))p)-- | A record of options that control how a path is stroked.-- @StrokeOpts@ is an instance of 'Default', so a @StrokeOpts@-- records can be created using @'with' { ... }@ notation.dataStrokeOptsa=StrokeOpts{vertexNames::[[a]]-- ^ Atomic names that should be assigned-- to the vertices of the path so that-- they can be referenced later. If-- there are not enough names, the extra-- vertices are not assigned names; if-- there are too many, the extra names-- are ignored. Note that this is a-- /list of lists/ of names, since paths-- can consist of multiple trails. The-- first list of names are assigned to-- the vertices of the first trail, the-- second list to the second trail, and-- so on.---- The default value is the empty list.,queryFillRule::FillRule-- ^ The fill rule used for determining-- which points are inside the path.-- The default is 'Winding'. NOTE: for-- now, this only affects the resulting-- diagram's 'Query', /not/ how it will-- be drawn! To set the fill rule-- determining how it is to be drawn,-- use the 'fillRule' function.}instanceDefault(StrokeOptsa)wheredef=StrokeOpts{vertexNames=[],queryFillRule=Winding}-- | A composition of 'stroke' and 'pathFromTrail' for conveniently-- converting a trail directly into a diagram.---- Note that a bug in GHC 7.0.1 causes a context stack overflow when-- inferring the type of 'stroke' and hence of @strokeT@ as well.-- The solution is to give a type signature to expressions involving-- @strokeT@, or (recommended) upgrade GHC (the bug is fixed in 7.0.2-- onwards).strokeT::(Renderable(PathR2)b)=>TrailR2->DiagrambR2strokeT=stroke.pathFromTrail-- | A composition of 'stroke'' and 'pathFromTrail' for conveniently-- converting a trail directly into a diagram.strokeT'::(Renderable(PathR2)b,IsNamea)=>StrokeOptsa->TrailR2->DiagrambR2strokeT'opts=stroke'opts.pathFromTrail-------------------------------------------------------------- Inside/outside testing-------------------------------------------------------------- | Enumeration of algorithms or \"rules\" for determining which-- points lie in the interior of a (possibly self-intersecting)-- closed path.dataFillRule=Winding-- ^ Interior points are those with a nonzero-- /winding/ /number/. See-- <http://en.wikipedia.org/wiki/Nonzero-rule>.|EvenOdd-- ^ Interior points are those where a ray-- extended infinitely in a particular-- direction crosses the path an odd number-- of times. See-- <http://en.wikipedia.org/wiki/Even-odd_rule>.runFillRule::FillRule->P2->PathR2->BoolrunFillRuleWinding=isInsideWindingrunFillRuleEvenOdd=isInsideEvenOddnewtypeFillRuleA=FillRuleA(LastFillRule)deriving(Typeable,Semigroup)instanceAttributeClassFillRuleA-- | Extract the fill rule from a 'FillRuleA' attribute.getFillRule::FillRuleA->FillRulegetFillRule(FillRuleA(Lastr))=r-- | Specify the fill rule that should be used for determining which-- points are inside a path.fillRule::HasStylea=>FillRule->a->afillRule=applyAttr.FillRuleA.Lastcross::R2->R2->Doublecross(x,y)(x',y')=x*y'-y*x'-- XXX link to more info on this-- | Test whether the given point is inside the given (closed) path,-- by testing whether the point's /winding number/ is nonzero. Note-- that @False@ is /always/ returned for /open/ paths, regardless of-- the winding number.isInsideWinding::P2->PathR2->BoolisInsideWindingp=(/=0).crossingsp-- | Test whether the given point is inside the given (closed) path,-- by testing whether a ray extending from the point in the positive-- x direction crosses the path an even (outside) or odd (inside)-- number of times. Note that @False@ is /always/ returned for-- /open/ paths, regardless of the number of crossings.isInsideEvenOdd::P2->PathR2->BoolisInsideEvenOddp=odd.crossingsp-- | Compute the sum of /signed/ crossings of a path as we travel in the-- positive x direction from a given point.crossings::P2->PathR2->Intcrossingsp=F.sum.map(trailCrossingsp).pathTrails-- | Compute the sum of signed crossings of a trail starting from the-- given point in the positive x direction.trailCrossings::P2->(P2,TrailR2)->Int-- open trails have no inside or outside, so don't contribute crossingstrailCrossings_(_,t)|not(isClosedt)=0trailCrossingsp@(P(x,y))(start,tr)=sum.maptest$fixTrailstarttrwheretest(FLineara@(P(_,ay))b@(P(_,by)))|ay<=y&&by>y&&isLeftab>0=1|by<=y&&ay>y&&isLeftab<0=-1|otherwise=0testc@(FCubic(Px1@(_,x1y))(Pc1@(_,c1y))(Pc2@(_,c2y))(Px2@(_,x2y)))=sum.maptestT$tswherets=filter(liftA2(&&)(>=0)(<=1))$cubForm(-x1y+3*c1y-3*c2y+x2y)(3*x1y-6*c1y+3*c2y)(-3*x1y+3*c1y)(x1y-y)testTt=let(P(px,_))=c`fAtParam`tinifpx>xthensignFromDerivAttelse0signFromDerivAtt=let(dx,dy)=(3*t*t)*^((-1)*^x1^+^3*^c1^-^3*^c2^+^x2)^+^(2*t)*^(3*^x1^-^6*^c1^+^3*^c2)^+^((-3)*^x1^+^3*^c1)ang=atan2dydxincase()of_|(0<ang&&ang<tau/2&&t<1)->1|(-tau/2<ang&&ang<0&&t>0)->-1|otherwise->0isLeftab=cross(b.-.a)(p.-.a)-------------------------------------------------------------- Clipping ------------------------------------------------------------------------------------------------------------ | @Clip@ tracks the accumulated clipping paths applied to a-- diagram. Note that the semigroup structure on @Clip@ is list-- concatenation, so applying multiple clipping paths is sensible.-- The clipping region is the intersection of all the applied-- clipping paths.newtypeClip=Clip{getClip::[PathR2]}deriving(Typeable,Semigroup)instanceAttributeClassCliptypeinstanceVClip=R2instanceTransformableClipwheretransformt(Clipps)=Clip(transformtps)-- | Clip a diagram by the given path:---- * Only the parts of the diagram which lie in the interior of the-- path will be drawn.---- * The bounding function of the diagram is unaffected.clipBy::(HasStylea,Va~R2)=>PathR2->a->aclipBy=applyTAttr.Clip.(:[])-- XXX Should include a 'clipTo' function which clips a diagram AND-- restricts its bounding function. It will have to take a *pointwise-- minimum* of the diagram's current bounding function and the path's-- bounding function. Not sure of the best way to do this at the moment.