implicit-0.4.0.0: Graphics/Implicit/ObjectUtil/GetBox2.hs
{- ORMOLU_DISABLE -}
-- Implicit CAD. Copyright (C) 2011, Christopher Olah (chris@colah.ca)
-- Copyright 2016, Julia Longtin (julial@turinglace.com)
-- Released under the GNU AGPLV3+, see LICENSE
module Graphics.Implicit.ObjectUtil.GetBox2 (getBox2, getBox2R) where
import Prelude(pure, fmap, Eq, (==), (.), (<$>), (||), unzip, minimum, maximum, ($), (/), (-), (+), (*), cos, sin, sqrt, min, max, (<), (<>), pi, atan2, (==), (>), show, (&&), otherwise, error)
import Graphics.Implicit.Definitions
( SymbolicObj2(Square, Circle, Polygon, Rotate2, Transform2, Shared2),
SharedObj(IntersectR, Complement, UnionR, DifferenceR),
Box2,
ℝ2,
ℝ,
minℝ )
import Data.Fixed (mod')
import Graphics.Implicit.ObjectUtil.GetBoxShared (emptyBox, corners, outsetBox, intersectBoxes, pointsBox, getBoxShared, unionBoxes)
-- To construct vectors of ℝs.
import Linear (V2(V2), V3(V3))
import qualified Linear
-- Get a Box2 around the given object.
getBox2 :: SymbolicObj2 -> Box2
-- Primitives
getBox2 (Square size) = (pure 0, size)
getBox2 (Circle r) = (pure (-r), pure r)
getBox2 (Polygon points) = pointsBox points
-- (Rounded) CSG
-- Simple transforms
getBox2 (Rotate2 θ symbObj) =
let rotate (V2 x y) = V2 (x*cos θ - y*sin θ) (x*sin θ + y*cos θ)
in pointsBox $ fmap rotate $ corners $ getBox2 symbObj
getBox2 (Transform2 m symbObj) =
let box = getBox2 symbObj
augment (V2 x y) = (V3 x y 1)
normalize (V3 x y w) = (V2 (x/w) (y/w))
in pointsBox $ normalize . (m Linear.!*) . augment <$> corners box
getBox2 (Shared2 obj) = getBoxShared obj
-- | Define a Box2 around the given object, and the space it occupies while rotating about the center point.
-- Note: No implementations for Square, Translate2, or Scale2 as they would be identical to the fallthrough.
getBox2R :: SymbolicObj2 -> ℝ -> Box2
getBox2R (Circle r) _ = getBox2 $ Circle r
getBox2R (Polygon points) deg =
let
pointRBoxes = [ pointRBox point deg | point <- points ]
(pointValsMin, pointValsMax) = unzip pointRBoxes
unbox :: ℝ2 -> (ℝ, ℝ)
unbox (V2 x y) = (x, y)
(pointValsX, pointValsY) = unzip $ fmap unbox $ pointValsMin <> pointValsMax
in
(V2 (minimum pointValsX)( minimum pointValsY), V2 (maximum pointValsX) (maximum pointValsY))
getBox2R (Shared2 (Complement symObj)) _ = getBox2 $ Shared2 (Complement symObj)
getBox2R (Shared2 (UnionR r symObjs)) deg =
unionBoxes r $ fmap (`getBox2R` deg) symObjs
getBox2R (Shared2 (DifferenceR _ symObj _)) deg = getBox2R symObj deg
getBox2R (Shared2 (IntersectR r symObjs)) deg =
let
boxes = [ getBox2R obj deg| obj <- symObjs ]
in
outsetBox r $ intersectBoxes boxes
-- FIXME: implement Rotate2.
-- Fallthrough: rotate the points of the containing box. no rounding.
getBox2R symObj deg =
let
origBox = getBox2 symObj
points = corners origBox
in
getBox2R (Polygon points) deg
data Quadrant = UpperRight | UpperLeft | LowerRight | LowerLeft
deriving Eq
data Axis = PosX | PosY | NegX | NegY
deriving Eq
data Position = OnAxis Axis | InQuadrant Quadrant | CenterPoint
data HasRotation = Rotation Direction | None
deriving Eq
data Direction = Clockwise | CounterClockwise
deriving Eq
-- | put a box around a point, and all of the locations it will be at during an x degree arc around (0,0).
pointRBox :: ℝ2 -> ℝ -> Box2
pointRBox (V2 xStart yStart) travel =
let
k :: ℝ
k = pi/180
-- determine the distance of our input point from from the axis of rotation.
distance = sqrt $ xStart*xStart + yStart*yStart
-- radian starting position.
θstart = atan2 yStart xStart
-- logical starting position
startPosition = positionOf distance $ absrad θstart
-- take the input point. rotate it. see where it stops.
-- how far we should rotate our point.
rotationAmount = travel * k
-- what direction are we rotating.
rotationDirection = case travel of
polarity | polarity > 0 -> Rotation CounterClockwise
| polarity == 0 -> None
_ -> Rotation Clockwise
-- stopping position of our point.
θstop = absrad $ θstart + rotationAmount
stopPosition = positionOf distance θstop
(xStop, yStop) =
case positionOf distance θstop of
CenterPoint -> (0,0)
OnAxis PosX -> (distance,0)
OnAxis PosY -> (0,distance)
OnAxis NegX -> (-distance,0)
OnAxis NegY -> (0,-distance)
InQuadrant _ -> ( distance*cos θstop, distance*sin θstop)
-- observe what the initial position was, and what the end position is.
-- check which quadrants they're in, and what direction the rotation was.
(minX, minY, maxX, maxY) = (min xStart xStop, min yStart yStop, max xStart xStop, max yStart yStop)
positionOf :: ℝ -> ℝ -> Position
positionOf d θpos
| d < minℝ = CenterPoint
| θpos == 0 || θpos == 360*k = OnAxis PosX
| θpos == 90*k = OnAxis PosY
| θpos == 180*k || θpos == -0 = OnAxis NegX
| θpos == 270*k = OnAxis NegY
| θpos > 0 && θpos < 90*k = InQuadrant UpperRight
| θpos > 90*k && θpos < 180*k = InQuadrant UpperLeft
| θpos > 180*k && θpos < 270*k = InQuadrant LowerLeft
| θpos > 270*k && θpos < 360*k = InQuadrant LowerRight
| otherwise = error $ "illegal position in positionOf: " <> show (θpos*k) <> " pos: " <> show θpos <> " d: " <> show d
-- returns position around a circle in radians, from 0 to 2pi.
absrad :: ℝ -> ℝ
absrad rad
| rad > (360*k) = rad `mod'` (360*k)
| rad < 0 = absrad (360*k)+rad
| otherwise = rad
-- now, if you passed through an axis, then the box must be expanded to include distance from axis of rotation in that direction.
-- otherwise, put a box around the start and stop positions.
distanceBox :: Box2
distanceBox = (pure (-distance), pure distance)
noAxis :: Quadrant -> Quadrant -> Direction -> ℝ -> Box2
noAxis q1 q2 dir amount
| q1 == q2 && amount < 90*k && amount > -90*k = (V2 minX minY, V2 maxX maxY)
| dir == Clockwise && q1 == UpperLeft = oneAxis PosY q2 dir amount
| dir == Clockwise && q1 == LowerRight = oneAxis PosX q2 dir amount
| dir == Clockwise && q1 == LowerLeft = oneAxis NegY q2 dir amount
| dir == Clockwise && q1 == UpperRight = oneAxis NegX q2 dir amount
| dir == CounterClockwise && q1 == UpperRight = oneAxis PosX q2 dir amount
| dir == CounterClockwise && q1 == UpperLeft = oneAxis PosY q2 dir amount
| dir == CounterClockwise && q1 == LowerLeft = oneAxis NegX q2 dir amount
| dir == CounterClockwise && q1 == LowerRight = oneAxis NegY q2 dir amount
noAxis _ _ _ _ = distanceBox
oneAxis :: Axis -> Quadrant -> Direction -> ℝ -> Box2
oneAxis axis quadrant dir amount
| dir == Clockwise &&
amount < 90*k && amount > -90*k &&
((axis == PosX && quadrant == LowerRight) ||
(axis == NegY && quadrant == LowerLeft) ||
(axis == NegX && quadrant == UpperLeft) ||
(axis == PosY && quadrant == UpperRight)) = (V2 minX minY, V2 maxX maxY)
| dir == CounterClockwise &&
amount < 90*k && amount > -90*k &&
((axis == PosX && quadrant == UpperRight) ||
(axis == PosY && quadrant == UpperLeft) ||
(axis == NegX && quadrant == LowerLeft) ||
(axis == NegY && quadrant == LowerRight)) = (V2 minX minY, V2 maxX maxY)
| dir == Clockwise &&
((axis == PosX && quadrant == LowerLeft) ||
(axis == NegY && quadrant == UpperLeft) ||
(axis == NegX && quadrant == UpperRight) ||
(axis == PosY && quadrant == LowerRight)) = crossOne axis dir
| dir == CounterClockwise &&
((axis == PosX && quadrant == UpperLeft) ||
(axis == PosY && quadrant == LowerLeft) ||
(axis == NegX && quadrant == LowerRight) ||
(axis == NegY && quadrant == UpperRight)) = crossOne axis dir
| dir == Clockwise &&
((axis == PosX && quadrant == UpperLeft) ||
(axis == PosY && quadrant == LowerLeft) ||
(axis == NegX && quadrant == LowerRight) ||
(axis == NegY && quadrant == UpperRight)) = crossTwo axis dir
| dir == CounterClockwise &&
((axis == PosX && quadrant == LowerLeft) ||
(axis == NegY && quadrant == UpperLeft) ||
(axis == NegX && quadrant == UpperRight) ||
(axis == PosY && quadrant == LowerRight)) = crossTwo axis dir
| dir == Clockwise &&
((axis == PosX && quadrant == UpperRight) ||
(axis == PosY && quadrant == UpperLeft) ||
(axis == NegX && quadrant == LowerLeft) ||
(axis == NegY && quadrant == LowerRight)) = crossThree axis
| dir == CounterClockwise &&
((axis == PosX && quadrant == LowerRight) ||
(axis == NegY && quadrant == LowerLeft) ||
(axis == NegX && quadrant == UpperLeft) ||
(axis == PosY && quadrant == UpperRight)) = crossThree axis
| otherwise = distanceBox
twoAxis :: Axis -> Axis -> Direction -> Box2
twoAxis start stop dir
| (start == PosX && stop == NegX) ||
(start == PosY && stop == NegY) ||
(start == NegX && stop == PosX) ||
(start == NegY && stop == PosY) = crossOne start dir
twoAxis start stop dir
| (start == PosX && stop == NegY) ||
(start == NegY && stop == NegX) ||
(start == NegX && stop == PosY) ||
(start == PosY && stop == PosX) = if dir == Clockwise
then (V2 minX minY, V2 maxX maxY)
else crossTwo start dir
| (start == PosX && stop == PosY) ||
(start == PosY && stop == NegX) ||
(start == NegX && stop == NegY) ||
(start == NegY && stop == PosX) = if dir == CounterClockwise
then (V2 minX minY, V2 maxX maxY)
else crossTwo start dir
twoAxis _ _ _ = distanceBox
crossOne :: Axis -> Direction -> Box2
crossOne start dir
| (start == PosX && dir == Clockwise) ||
(start == NegX && dir == CounterClockwise) = mixWith [(0,-distance)]
| (start == NegY && dir == Clockwise) ||
(start == PosY && dir == CounterClockwise) = mixWith [(-distance, 0)]
| (start == NegX && dir == Clockwise) ||
(start == PosX && dir == CounterClockwise) = mixWith [(0, distance)]
| (start == PosY && dir == Clockwise) ||
(start == NegY && dir == CounterClockwise) = mixWith [( distance, 0)]
| otherwise = distanceBox
crossTwo :: Axis -> Direction -> Box2
crossTwo start dir
| (start == PosX && dir == Clockwise) ||
(start == PosY && dir == CounterClockwise) = mixWith [(-distance, 0), ( 0,-distance)]
| (start == PosY && dir == Clockwise) ||
(start == NegX && dir == CounterClockwise) = mixWith [( distance, 0), ( 0,-distance)]
| (start == NegX && dir == Clockwise) ||
(start == NegY && dir == CounterClockwise) = mixWith [( distance, 0), ( 0, distance)]
| (start == NegY && dir == Clockwise) ||
(start == PosX && dir == CounterClockwise) = mixWith [(-distance, 0), ( 0, distance)]
| otherwise = distanceBox
crossThree :: Axis -> Box2
crossThree PosX = mixWith [( 0, distance), (-distance, 0), ( 0,-distance)]
crossThree PosY = mixWith [(-distance, 0), ( 0,-distance), ( distance, 0)]
crossThree NegX = mixWith [( 0,-distance), ( distance, 0), ( 0, distance)]
crossThree NegY = mixWith [( distance, 0), ( 0, distance), (-distance, 0)]
-- TODO(sandy): clean me up to not use pairs
mixWith :: [(ℝ,ℝ)] -> Box2
mixWith points = (V2 (minimum xPoints) (minimum yPoints), V2 (maximum xPoints) (maximum yPoints))
where
(xPoints, yPoints) = unzip $ points <> [(xStart, yStart), (xStop, yStop)]
invertRotation :: Direction -> Direction
invertRotation Clockwise = CounterClockwise
invertRotation CounterClockwise = Clockwise
in
case rotationDirection of
None -> (V2 xStart yStart, V2 xStart yStart)
Rotation dir -> case rotationAmount of
amount | amount < 360*k && amount > -360*k ->
case startPosition of
CenterPoint -> emptyBox
OnAxis axis -> case stopPosition of
OnAxis stopaxis -> twoAxis axis stopaxis dir
InQuadrant stopquadrant -> oneAxis axis stopquadrant dir amount
CenterPoint -> emptyBox
InQuadrant quadrant -> case stopPosition of
OnAxis stopaxis -> oneAxis stopaxis quadrant (invertRotation dir) (-amount)
InQuadrant stopquadrant -> noAxis quadrant stopquadrant dir travel
CenterPoint -> emptyBox
_ ->
(pure (-distance), pure distance)