{-# LANGUAGE TemplateHaskell, StandaloneDeriving #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE DeriveGeneric #-} {-# OPTIONS_HADDOCK prune #-} module Penrose.Transforms where {- Naming conventions in this file: Postfixes specifying argument types: P: point S: segment B: blob (simply connected region represented by a list of vertices) G: polygon A: angle in degrees Other postfixes: Offs: offset, specifies exact separation amount Pad: padding, specifies minimum separation amount Prefix e: post-level energy When a function takes multiple blobs/polygons as inputs, name them as A, B, etc. in order When using angle to specify an axis, let theta be the input angle in radians (converted from degrees), then the axis in question is the line containing vector (cos theta, sin theta). -} import Penrose.Util import Debug.Trace import Data.List (nub, sort, findIndex, find, maximumBy, foldl') import qualified Data.Map.Strict as M import GHC.Generics import Data.Aeson (FromJSON, ToJSON, toJSON) --import Par default (Int, Float) -- a, b, c, d, e, f as here: -- https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform data HMatrix a = HMatrix { xScale :: a, -- a xSkew :: a, -- c -- What are c and b called? ySkew :: a, -- b yScale :: a, -- d dx :: a, -- e dy :: a -- f } deriving (Generic, Eq) instance Show a => Show (HMatrix a) where show m = "[ " ++ show (xScale m) ++ " " ++ show (xSkew m) ++ " " ++ show (dx m) ++ " ]\n" ++ " " ++ show (ySkew m) ++ " " ++ show (yScale m) ++ " " ++ show (dy m) ++ " \n" ++ " 0.0 0.0 1.0 ]" instance (FromJSON a) => FromJSON (HMatrix a) instance (ToJSON a) => ToJSON (HMatrix a) idH :: (Autofloat a) => HMatrix a idH = HMatrix { xScale = 1, xSkew = 0, ySkew = 0, yScale = 1, dx = 0, dy = 0 } -- First row, then second row hmToList :: (Autofloat a) => HMatrix a -> [a] hmToList m = [ xScale m, xSkew m, dx m, ySkew m, yScale m, dy m ] listToHm :: (Autofloat a) => [a] -> HMatrix a listToHm l = if length l /= 6 then error "wrong length list for hmatrix" else HMatrix { xScale = l !! 0, xSkew = l !! 1, dx = l !! 2, ySkew = l !! 3, yScale = l !! 4, dy = l !! 5 } hmDiff :: (Autofloat a) => HMatrix a -> HMatrix a -> a hmDiff t1 t2 = let (l1, l2) = (hmToList t1, hmToList t2) in norm $ l1 -. l2 applyTransform :: (Autofloat a) => HMatrix a -> Pt2 a -> Pt2 a applyTransform m (x, y) = (x * xScale m + y * xSkew m + dx m, x * ySkew m + y * yScale m + dy m) infixl ## (##) :: (Autofloat a) => HMatrix a -> Pt2 a -> Pt2 a (##) = applyTransform -- General functions to work with transformations -- Do t2, then t1. That is, multiply two homogeneous matrices: t1 * t2 composeTransform :: (Autofloat a) => HMatrix a -> HMatrix a -> HMatrix a composeTransform t1 t2 = HMatrix { xScale = xScale t1 * xScale t2 + xSkew t1 * ySkew t2, xSkew = xScale t1 * xSkew t2 + xSkew t1 * yScale t2, ySkew = ySkew t1 * xScale t2 + yScale t1 * ySkew t2, yScale = ySkew t1 * xSkew t2 + yScale t1 * yScale t2, dx = xScale t1 * dx t2 + xSkew t1 * dy t2 + dx t1, dy = yScale t1 * dy t2 + ySkew t1 * dx t2 + dy t1 } -- TODO: test that this gives expected results for two scalings, translations, rotations, etc. infixl 7 # (#) :: (Autofloat a) => HMatrix a -> HMatrix a -> HMatrix a (#) = composeTransform -- Compose all the transforms in RIGHT TO LEFT order: -- [t1, t2, t3] means "do t3, then do t2, then do t1" or "t1 * t2 * t3" composeTransforms :: (Autofloat a) => [HMatrix a] -> HMatrix a composeTransforms ts = foldr composeTransform idH ts -- Specific transformations rotationM :: (Autofloat a) => a -> HMatrix a rotationM radians = idH { xScale = cos radians, xSkew = -(sin radians), ySkew = sin radians, yScale = cos radians } translationM :: (Autofloat a) => Pt2 a -> HMatrix a translationM (x, y) = idH { dx = x, dy = y } scalingM :: (Autofloat a) => Pt2 a -> HMatrix a scalingM (cx, cy) = idH { xScale = cx, yScale = cy } rotationAboutM :: (Autofloat a) => a -> Pt2 a -> HMatrix a rotationAboutM radians (x, y) = -- Make the new point the new origin, do a rotation, then translate back composeTransforms [translationM (x, y), rotationM radians, translationM (-x, -y)] shearingX :: (Autofloat a) => a -> HMatrix a shearingX lambda = idH { xSkew = lambda } shearingY :: (Autofloat a) => a -> HMatrix a shearingY lambda = idH { ySkew = lambda } ------ Solve for final parameters -- See PR for documentation -- Note: returns angle in range [0, 2pi) paramsOf :: (Autofloat a) => HMatrix a -> (a, a, a, a, a) -- There could be multiple solutions paramsOf m = let (sx, sy) = (norm [xScale m, ySkew m], norm [xSkew m, yScale m]) in -- Ignore negative scale factors let theta = atan (ySkew m / (xScale m + epsd)) in -- Prevent atan(0/0) = NaN -- atan returns an angle in [-pi, pi] (sx, sy, theta, dx m, dy m) paramsToMatrix :: (Autofloat a) => (a, a, a, a, a) -> HMatrix a paramsToMatrix (sx, sy, theta, dx, dy) = -- scale then rotate then translate composeTransforms [translationM (dx, dy), rotationM theta, scalingM (sx, sy)] toParallelogram :: (Autofloat a) => (a, a, a, a, a, a) -> HMatrix a toParallelogram (w, h, rotation, x, y, shearAngle) = if shearAngle < 10**(-5) then error "shearAngle can't be 0, tangent will NaN" else composeTransforms [translationM (x, y), rotationM rotation, -- translationM (-w/2, -h/2), shearingX (1 / tan shearAngle), -- translationM (w/2, h/2), scalingM (w, h) ] unitSq :: (Autofloat a) => [Pt2 a] unitSq = [(0.5, 0.5), (-0.5, 0.5), (-0.5, -0.5), (0.5, -0.5)] circlePoly :: (Autofloat a) => a -> [Pt2 a] circlePoly r = let -- currently doesn't use r, so that #sides remains the same throughout opt. -- TODO: might want to depend on radius of the original circle in some way? sides :: Int sides = max 8 $ floor (64 / 4.0) indices :: [Int] indices = [0..sides-1] angles = map (\i -> (r2f i) / (r2f sides) * 2.0 * pi) indices pts = map (\a -> (cos a, sin a)) angles in pts -- Sample a circle about the origin with some density according to its radius sampleUnitCirc :: (Autofloat a) => a -> [Pt2 a] sampleUnitCirc r = if r < 0.01 then [(0, 0)] else [] testTriangle :: (Autofloat a) => [Pt2 a] testTriangle = [(0, 0), (100, 0), (50, 50)] testNonconvex :: (Autofloat a) => [Pt2 a] testNonconvex = [(0, 0), (100, 0), (50, 50), (100, 100), (0, 100)] -- TODO: the samples be transformed along by transforming here? transformPoly :: (Autofloat a) => HMatrix a -> Polygon a -> Polygon a transformPoly m (blobs, holes, _, samples) = let transformedBlobs = map (map (applyTransform m)) blobs in ( transformedBlobs, map (map (applyTransform m)) holes, getBBox (concat transformedBlobs), map (applyTransform m) samples ) transformSimplePoly :: (Autofloat a) => HMatrix a -> [Pt2 a] -> [Pt2 a] transformSimplePoly m = map (applyTransform m) -- Pointing to the right. Note: doesn't try to approximate the "inset" part of the arrowhead rtArrowheadPoly :: (Autofloat a) => a -> [Pt2 a] rtArrowheadPoly c = let x = 2 * c in -- c is the thickness of the line let twox = x*2 in [(-x, x/4), -- Stem top (-twox, twox), -- Top left (twox, 0), -- Point of arrownead (-twox, -twox), -- Bottom left (-x, -x/4)] -- Stem bottom ltArrowheadPoly :: (Autofloat a) => a -> [Pt2 a] ltArrowheadPoly c = reverse $ map flipX $ rtArrowheadPoly c where flipX (x, y) = (-x, y) -- Reverse so the line polygon can join the stem bottom with the stem bottom -- | Make a rectangular polygon of a line segment, accounting for its thickness and arrowheads -- (Is this usable with autodiff?) extrude :: (Autofloat a) => a -> Pt2 a -> Pt2 a -> Bool -> Bool -> [Pt2 a] extrude c x y leftArr rightArr = let dir = normalize' (y -: x) normal = rot90 dir -- normal vector to the line segment offset = (c / 2) *: normal -- find offset for each endpoint halfLen = mag (y -: x) / 2.0 -- Calculate arrowhead position trLeft = (-halfLen) *: dir trRight = halfLen *: dir left = if leftArr then translate2 trLeft (ltArrowheadPoly c) else [x -: offset, x +: offset] -- Note: order matters for making the polygon right = if rightArr then translate2 trRight (rtArrowheadPoly c) else [y +: offset, y -: offset] in left ++ right ------ Energies on polygons ------ ---- some helpers below ---- type LineSeg a = (Pt2 a, Pt2 a) type Blob a = [Pt2 a] -- temporary type for polygon. Connected, no holes -- TODO: assuming that the positive shapes don't overlap and the negative shapes don't overlap (check this) -- (positive shapes, negative shapes, bbox, samples) type Polygon a = ([Blob a], [Blob a], (Pt2 a, Pt2 a), [Pt2 a]) emptyPoly :: Autofloat a => Polygon a emptyPoly = ([], [], ((posInf, posInf), (negInf, negInf)), []) toPoly :: Autofloat a => [Pt2 a] -> Polygon a toPoly pts = ([pts], [], getBBox pts, sampleB numSamples pts) posInf :: Autofloat a => a posInf = 1 / 0 negInf :: Autofloat a => a negInf = -1 / 0 -- input point, query parametric t gettPS :: Autofloat a => Pt2 a -> LineSeg a -> a gettPS p (a,b) = let v_ab = b -: a projl = v_ab `dotv` (p -: a) in projl / (magsq v_ab) closestPointPS :: Autofloat a => Pt2 a -> LineSeg a -> Pt2 a closestPointPS p (a,b) = let t = gettPS p (a,b) in if t<0.0 then a else if t>=1 then b else let v_ab = b -: a in a +: (t *: v_ab) closestPointPG :: Autofloat a => Pt2 a -> Polygon a -> Pt2 a closestPointPG p poly = let segments = getSegmentsG poly -- mapf s = closestPointPS p s cps = map mapf segments -- parmap? -- redf p1 p2 = let d1 = dsqPP p1 p d2 = dsqPP p2 p in if d1 <= d2 then p1 else p2 in foldl' redf (cps!!0) cps -- reduce? normS :: Autofloat a => LineSeg a -> Pt2 a normS (p1, p2) = normalize' $ rot90 $ p2 -: p1 -- test if two segments intersect. Doesn't calculate for ix point though. ixSS :: Autofloat a => LineSeg a -> LineSeg a -> Bool ixSS (a,b) (c,d) = let ncd = rot90 $ d -: c a_cd = ncd `dotv` (a -: c) b_cd = ncd `dotv` (b -: c) nab = rot90 $ b -: a c_ab = nab `dotv` (c -: a) d_ab = nab `dotv` (d -: a) in ((a_cd>=0&&b_cd<=0) || (a_cd<=0&&b_cd>=0)) && ((c_ab>=0&&d_ab<=0) || (c_ab<=0&&d_ab>=0)) getSegmentsB :: Autofloat a => Blob a -> [LineSeg a] getSegmentsB pts = let pts' = rotateList pts in zip pts pts' getSegmentsG :: Autofloat a => Polygon a -> [LineSeg a] getSegmentsG (bds, hs, _, _) = let bdsegments = map (\b->getSegmentsB b) bds hsegments = map (\h->getSegmentsB h) hs in concat [concat bdsegments, concat hsegments] -- OLD -- blob inside/outside test wo testing bbox first isInB'' :: Autofloat a => Blob a -> Pt2 a -> Bool isInB'' pts (x0,y0) = let diffp = map (\(x,y)->(x-x0,y-y0)) pts getAngle (x,y) = atan2 y x angles = map getAngle diffp angles' = rotateList angles sweeps = map (\(a,b)->b-a) $ zip angles angles' adjust sweep = if sweep>pi then sweep-2*pi else if sweep<(-pi) then 2*pi+sweep else sweep sweepAdjusted = map adjust sweeps -- if inside pos poly, res would be 2*pi, -- if inside neg poly, res would be -2*pi, -- else res would be 0 res = foldl' (+) 0.0 sweepAdjusted in res>pi || res<(-pi) -- A cheaper implementation of inside/outside test, found at -- http://geomalgorithms.com/a03-_inclusion.html isInB' :: Autofloat a => Blob a -> Pt2 a -> Bool isInB' pts p@(x0, y0) = let segments = getSegmentsB pts wnf (a@(x1,y1), b@(x2,y2)) = if (y1>y0 && y2>y0) || (y1 0 then 1 else (-1) wn = foldl' (+) 0 $ map wnf segments in if wn==0 then False else True -- general, direct inside/outside test isInG' :: Autofloat a => Polygon a -> Pt2 a -> Bool isInG' (bds, hs, _, _) p = let inb = foldl' (||) False $ map (\b->isInB' b p) bds inh = foldl' (||) False $ map (\h->isInB' h p) hs in (inb) && (not inh) -- inside/outside test w polygon by first testing against bbox. isInG :: Autofloat a => Polygon a -> Pt2 a -> Bool isInG poly@(_,_,bbox,_) p = if inBBox bbox p then isInG' poly p else False -- isOutG :: Autofloat a => Polygon a -> Pt2 a -> a -> Bool -- isOutG poly p ofs = (not $ isInG' poly p) && (dsqGP poly p 0 >= ofs) getBBox :: Autofloat a => Blob a -> (Pt2 a, Pt2 a) getBBox pts = let foldfn :: Autofloat a => (Pt2 a, Pt2 a) -> Pt2 a -> (Pt2 a, Pt2 a) foldfn ((xlo,ylo), (xhi,yhi)) (x, y) = ( (min xlo x, min ylo y), (max xhi x, max yhi y) ) in foldl' foldfn ((posInf, posInf), (negInf, negInf)) pts -- returns the center of the polygon bbox getCenter :: Autofloat a => Polygon a -> Pt2 a getCenter (_,_,((xlo,ylo), (xhi,yhi)),_) = ((xhi+xlo)/2, (yhi+ylo)/2) getDiameter2' :: Autofloat a => [Pt2 a] -> a getDiameter2' pts = case pts of [] -> 0 p:pts -> foldl' max 0 $ map (dsqPP p) pts -- a shape's diameter is defined as the length of the longest segment connecting -- p1, p2, both of which are on the shape's boundary. getDiameter :: Autofloat a => Polygon a -> a getDiameter (bds,hs,_,_) = let vertices = (concat bds) ++ (concat hs) in sqrt $ getDiameter2' vertices -- true if in bbox and far enough from boundary inBBox :: Autofloat a => (Pt2 a, Pt2 a) -> Pt2 a -> Bool inBBox ((xlo, ylo), (xhi, yhi)) (x,y) = let res = x >= xlo && x <= xhi && y >= ylo && y <= yhi in res scaleB :: Autofloat a => a -> Blob a -> Blob a scaleB k b = map (scaleP k) b -- sample points along segment with interval. sampleS :: Autofloat a => a -> LineSeg a -> [Pt2 a] sampleS numSamplesf (a, b) = let -- l = mag $ b -: a numSamples :: Int numSamples = (floor numSamplesf) + 1--floor $ l/interval inds = map realToFrac [0..numSamples-1] ks = map (/(realToFrac numSamples)) $ inds in map (lerpP a b) ks sampleB :: Autofloat a => Int -> Blob a -> [Pt2 a] sampleB numSamples blob = let numSamplesf = r2f numSamples segments = getSegmentsB blob circumfrence = foldl' (+) 0.0 $ map (\(a,b)->dist a b) segments seglengths = map (\(a,b)->dist a b) $ segments samplesEach = map (\l->l/circumfrence*numSamplesf) seglengths zp = zip segments samplesEach in concat $ map (\(seg, num) -> sampleS num seg) zp -- TODO: be absolutely sure #samples are same as input # sampleG :: Autofloat a => Int -> Polygon a -> [Pt2 a] sampleG numSamples poly@(bds, hs, _, _) = let numSamplesf = r2f numSamples segments = getSegmentsG poly circumfrence = foldl' (+) 0.0 $ map (\(a,b)->dist a b) segments seglengths = map (\(a,b)->dist a b) $ segments samplesEach = map (\l->l/circumfrence*numSamplesf) seglengths zp = zip segments samplesEach in concat $ map (\(seg, num) -> sampleS num seg) zp ---- dsq functions ---- dsqPP :: Autofloat a => Pt2 a -> Pt2 a -> a dsqPP a b = magsq $ b -: a dsqSP :: Autofloat a => LineSeg a -> Pt2 a -> a dsqSP (a,b) p = let t = gettPS p (a,b) in if t<0 then dsqPP a p else if t>1 then dsqPP b p else (**2) $ (normS (a,b)) `dotv` (p -: a) dsqSS :: Autofloat a => LineSeg a -> LineSeg a -> a dsqSS (a,b) (c,d) = if ixSS (a,b) (c,d) then 0 else let da = dsqSP (c,d) a db = dsqSP (c,d) b dc = dsqSP (a,b) c dd = dsqSP (a,b) d in min (min da db) (min dc dd) dsqBP :: Autofloat a => Blob a -> Pt2 a -> a dsqBP b p = let segments = getSegmentsB b -- min dist to vertex d2v = foldl' min posInf $ map (\q -> magsq $ p -: q) b -- min dist to segment (if closest point falls within segment) d2s = foldl' min posInf $ map (\s@(a,b) -> let t = gettPS p s in if t>1 || t<0 then posInf else (**2) $ (normS (a,b)) `dotv` (p -: a)) segments in min d2v d2s dsqGP :: Autofloat a => Polygon a -> Pt2 a -> a dsqGP (bds, hs, _, _) p = let dsqBD = foldl' min posInf $ map (\b -> dsqBP b p) bds dsqHS = foldl' min posInf $ map (\h -> dsqBP h p) hs in min dsqBD dsqHS -- signed distance squared between polygons: -- only the magnitude matches with dsq. Negative when inside A. signedDsqGP :: Autofloat a => Polygon a -> Pt2 a -> a signedDsqGP poly p = let dsq = dsqGP poly p inside = if dsq < epsd then True else isInG poly p in if inside then -dsq else dsq dsqBS :: Autofloat a => Blob a -> LineSeg a -> a dsqBS b s = foldl' min posInf $ map (\e -> dsqSS e s) $ getSegmentsB b dsqBB :: Autofloat a => Blob a -> Blob a -> a dsqBB b1 b2 = let min1 = foldl' min posInf $ map (\e -> dsqBS b2 e) $ getSegmentsB b1 in min1 -- distance squared dsqGG :: Autofloat a => Polygon a -> Polygon a -> a dsqGG (bds1, hs1, _, _) (bds2, hs2, _, _) = let b1b2 = foldl' min posInf $ map (\b->foldl' min posInf $ map (\b'->dsqBB b b') bds1) bds2 b1h2 = foldl' min posInf $ map (\b->foldl' min posInf $ map (\b'->dsqBB b b') bds1) hs2 b2b1 = foldl' min posInf $ map (\b->foldl' min posInf $ map (\b'->dsqBB b b') bds2) bds1 b2h1 = foldl' min posInf $ map (\b->foldl' min posInf $ map (\b'->dsqBB b b') bds2) hs1 in min (min b1b2 b1h2) (min b2b1 b2h1) -- (helper) encourages the line containing two points to be parallel to the specified axis. alignPPA :: Autofloat a => Pt2 a -> Pt2 a -> a -> a alignPPA a b angle = let angle_radians = angle * pi / 180.0 dirN = rot90 (cos angle_radians, sin angle_radians) in (**2) $ (a `dotv` dirN) - (b `dotv` dirN) -- (helper) Let a' and b' be projections of a and b onto the axis. Encourages positions of a, b -- such that a' appears before b' on the axis -- ex: when angle = 0, encourages a to be on the left of b. -- when angle = 180, encourages a to be on the right of b orderPPA :: Autofloat a => Pt2 a -> Pt2 a -> a -> a orderPPA a b angle = let angle_radians = angle * pi / 180.0 dir = (cos angle_radians, sin angle_radians) in (**2) $ max 0 $ (a `dotv` dir) - (b `dotv` dir) -- samples per polygon numSamples :: Int numSamples = 200 ------------ some energy "building blocks" ------------ ---- type 1: dsq integral along boundary functions ---- -- penalize part of B's boundary that's inside A dsqBinA :: Autofloat a => Polygon a -> Polygon a -> a dsqBinA bA bB@(_,_,_,samplesB) = let samplesIn = filter (\p -> isInG bA p) $ samplesB in foldl' (+) 0.0 $ map (\p -> dsqGP bA p) samplesIn -- penalize part of B's boundary that's outside A dsqBoutA :: Autofloat a => Polygon a -> Polygon a -> a dsqBoutA bA bB@(_,_,_,samplesB) = let samplesOut = filter (\p -> not $ isInG bA p) $ samplesB in foldl' (+) 0.0 $ map (\p -> dsqGP bA p) samplesOut ---- type 2: min/max dsq on boundary functions ---- -- minimum (signed distance squared) between polygons based on sampling minSignedDsqGG :: Autofloat a => Polygon a -> Polygon a -> a minSignedDsqGG polyA polyB@(_,_,_,samplesB) = foldl' min posInf $ map (signedDsqGP polyA) samplesB -- maximum (signed distance squared) between polygons based on sampling maxSignedDsqGG :: Autofloat a => Polygon a -> Polygon a -> a maxSignedDsqGG polyA polyB@(_,_,_,samplesB) = foldl' max negInf $ map (signedDsqGP polyA) samplesB ----------------- top-level query energies ----------------- ---- Energies using min/max dsq on boundary (type 2) ---- -- eBinAOffs, eBoutAOffs: Energy lowest when minimum/maximum signed distance is at ofs pixels. -- Both functions have similar runtime compared to other inside/outside energies -- Both are a bit "unstable" (shapes make unexpected big jumps), likely because of how they're defined -- to consider only contribution from one sample. -- Lowest when A contains B with exactly ofs number of pixels between their boundaries. -- ofs > 0: A contains B with ofs pixels of padding (general use case) -- ofs < 0: A doesn't completely contain B, the point on B furthest from A is |ofs| pixels away. -- ofs = 0: an alternative containment+tangent energy eBinAOffs :: Autofloat a => Polygon a -> Polygon a -> a -> a eBinAOffs polyA polyB ofs = let sdsq = maxSignedDsqGG polyA polyB sign = if sdsq >= 0 then 1.0 else -1.0 sdist = (*sign) $ sqrt $ abs sdsq in (sdist + ofs)**2 -- Lowest when A, B disjoint with exactly ofs number of pixels between their boundaries. -- ofs > 0: A, B disjoint with ofs pixels of padding (general use case) -- ofs < 0: A and B intersect, B "penetrates into A" |ofs| pixels -- ofs = 0: an alternative disjoint+tangent energy eBoutAOffs :: Autofloat a => Polygon a -> Polygon a -> a -> a eBoutAOffs polyA polyB ofs = let sdsq = minSignedDsqGG polyA polyB sign = if sdsq >= 0 then 1.0 else -1.0 sdist = (*sign) $ sqrt $ abs sdsq in (sdist - ofs)**2 -- energy lowest when either of the above two energies are lowest -- used for specifying amt of distance to boundary but don't care about inside/outside -- ex: when placing labels eOffs :: Autofloat a => Polygon a -> Polygon a -> a -> a eOffs polyA polyB ofs = let eIn = eBinAOffs polyA polyB ofs eOut = eBoutAOffs polyA polyB ofs in min eIn eOut -- Same as eOffs except B is a point eOffsP :: Autofloat a => Polygon a -> Pt2 a -> a -> a eOffsP poly pt ofs = let d = sqrt $ dsqGP poly pt in (d - ofs)**2 -- Same as eOffs except both inputs are points eOffsPP :: Autofloat a => Pt2 a -> Pt2 a -> a -> a eOffsPP p1 p2 ofs = let d = sqrt $ dsqPP p1 p2 in (d - ofs)**2 -- eBinAPad, eBoutAPad: Energy lowest when minimum/maximum signed distance is at least ofs pixels -- (compare to eBinAOffs, eBoutAOffs) -- ofs > 0: A contains B with at least ofs pixels of padding (general use case) -- ofs = 0: an alternative containment energy eBinAPad :: Autofloat a => Polygon a -> Polygon a -> a -> a eBinAPad polyA polyB ofs = let sdsq = maxSignedDsqGG polyA polyB sign = if sdsq >= 0 then 1.0 else -1.0 sdist = (*sign) $ sqrt $ abs sdsq in (max 0 $ sdist + ofs)**2 -- ofs > 0: A, B disjoint with at least ofs pixels of padding (general use case) -- ofs = 0: an alternative disjoint energy eBoutAPad :: Autofloat a => Polygon a -> Polygon a -> a -> a eBoutAPad polyA polyB ofs = let sdsq = minSignedDsqGG polyA polyB sign = if sdsq >= 0 then 1.0 else -1.0 sdist = (*sign) $ sqrt $ abs sdsq in (min 0 $ sdist - ofs)**2 -- energy lowest when either eBinAPad or eBoutAPad is lowest -- used for specifying amt of minimum separation to boundary but don't care about inside/outside ePad :: Autofloat a => Polygon a -> Polygon a -> a -> a ePad polyA polyB ofs = let eIn = eBinAPad polyA polyB ofs eOut = eBoutAPad polyA polyB ofs in min eIn eOut ---- energies based on integral of dsq along boundary ---- -- A contain B eAcontainB :: Autofloat a => Polygon a -> Polygon a -> a eAcontainB bA bB = let eAinB = dsqBinA bB bA eBoutA = dsqBoutA bA bB in eAinB + eBoutA -- A, B disjoint. Might need something better, as not all local mins correspond to satisfaction of obj. -- resulting in two shapes overlap even more eABdisj :: Autofloat a => Polygon a -> Polygon a -> a eABdisj bA bB = let eAinB = dsqBinA bB bA eBinA = dsqBinA bA bB in eAinB + eBinA {- commented these out bc are just addition of above energies. -- A and B tangent, B inside A eBinAtangent :: Autofloat a => Polygon a -> Polygon a -> a eBinAtangent bA bB = let eContainment = eAcontainB bA bB eABbdix = dsqGG bA bB in eContainment + eABbdix -- A and B tangent, B outside A eBoutAtangent :: Autofloat a => Polygon a -> Polygon a -> a eBoutAtangent bA bB = let eDisjoint = eABdisj bA bB eABbdix = dsqGG bA bB in eDisjoint + eABbdix -} ---- Energies defined on polygon size (diameter), see function getDiameter ---- -- penalize if the diameter of poly is too large. eMaxSize :: Autofloat a => Polygon a -> a -> a eMaxSize poly size = let d = getDiameter poly in (**2) $ max 0 $ d - size -- penalize if the diameter of poly is too small. eMinSize :: Autofloat a => Polygon a -> a -> a eMinSize poly size = let d = getDiameter poly in (**2) $ max 0 $ size - d -- penalize if two shapes have different diameters eSameSize :: Autofloat a => Polygon a -> Polygon a -> a eSameSize bA bB = let d1 = getDiameter bA d2 = getDiameter bB in (d1 - d2) ** 2 -- penalize if A is larger than B eSmallerThan :: Autofloat a => Polygon a -> Polygon a -> a eSmallerThan bA bB = let d1 = getDiameter bA d2 = getDiameter bB in (max 0 $ d1 - d2) ** 2 -- no penalty if d1 <= d2 ---- Other energies (related to alignment and ordering) ---- -- currently uses center of bbox to represent polygon position. -- encourages A and B to align along some axis (input angle in degrees) -- in other words, encourages the line connecting A and B to be parallel to the given axis eAlign :: Autofloat a => Polygon a -> Polygon a -> a -> a eAlign bA bB angle = alignPPA (getCenter bA) (getCenter bB) angle -- encourages A and B to order along some axis (input angle in degrees) -- more abt ordering near function orderPPA eOrder :: Autofloat a => Polygon a -> Polygon a -> a -> a eOrder bA bB angle = orderPPA (getCenter bA) (getCenter bB) angle