aboutsummaryrefslogtreecommitdiff
path: root/src/Geometry.hs
blob: dd8d8e178f7d3375fbaa57bd1715cecbed3a59c9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
module Geometry
  ( Line (..),
    lineThrough2Points,
    lineThroughPointWithAngle,
    LineSide (..),
    sideOfLine,
    proj,
    perp',
    orthoproj,
    distPointLine,
    distPointSegment,
    rotate,
    -- Rectangle types
    GeometryRectangle (..),
    rectangleToGeometry,
    geometryToRectangle,
    -- Intersection
    IntersectionResult (..),
    getIntersectingRectangles,
  )
where

import Data.Int (Int32)
import Data.List (sortOn)
import Data.Maybe (catMaybes, listToMaybe)
import Graphics.X11 (Rectangle (..))
import Prelude hiding (subtract)

-- | A line in parametric form: (x + t*dx, y + t*dy) for t ∈ ℝ
data Line a = Line
  { x :: !a,
    y :: !a,
    dx :: !a,
    dy :: !a
  }
  deriving (Eq, Show, Read)

-- | Construct a line passing through two points
lineThrough2Points :: (Num a, Ord a) => (a, a) -> (a, a) -> Line a
lineThrough2Points (x1, y1) (x2, y2) =
  Line
    { x = x1,
      y = y1,
      dx = x2 - x1,
      dy = y2 - y1
    }

-- | Construct a line through a point with a given angle (in radians)
--   Angle is measured counterclockwise from the positive x-axis
lineThroughPointWithAngle :: (Floating a, Ord a) => (a, a) -> a -> Line a
lineThroughPointWithAngle (x, y) angle =
  Line
    { x = x,
      y = y,
      dx = cos angle,
      dy = sin angle
    }

-- | Which side of the line is a point on?
data LineSide
  = LeftSide
  | RightSide
  | OnLine
  deriving (Eq, Show)

-- | Determine which side of the line a point lies on
--   Uses the cross product to determine orientation
sideOfLine :: (Fractional a, Ord a) => Line a -> (a, a) -> LineSide
sideOfLine line (px, py) =
  case compare cross 0 of
    GT -> LeftSide
    LT -> RightSide
    EQ -> OnLine
  where
    -- Vector from line origin to point
    relX = px - x line
    relY = py - y line
    -- Cross product in 2D: (dx, dy) × (px, py) = dx*py - dy*px
    cross = dx line * relY - dy line * relX

-- | Project point onto line
proj :: (Fractional a, Ord a) => Line a -> (a, a) -> (a, a)
proj line (px, py) =
  (x line + t * dx line, y line + t * dy line)
  where
    -- Vector from line origin to point
    relX = px - x line
    relY = py - y line
    -- t = (rel · direction) / (direction · direction)
    dotProd = relX * dx line + relY * dy line
    dirSq = dx line * dx line + dy line * dy line
    t = dotProd / dirSq

-- | Perpendicular vector from point to line
perp' :: (Fractional a, Ord a) => Line a -> (a, a) -> (a, a)
perp' line point =
  (px - projX, py - projY)
  where
    (projX, projY) = proj line point
    (px, py) = point

-- | Orthogonal projection (same as proj, but returns the projected point)
orthoproj :: (Fractional a, Ord a) => Line a -> (a, a) -> (a, a)
orthoproj = proj

-- | Distance from point to line
distPointLine :: (Floating a, Ord a) => Line a -> (a, a) -> a
distPointLine line point =
  sqrt (dx * dx + dy * dy)
  where
    (dx, dy) = perp' line point

-- | Distance from point to line segment (not infinite line)
distPointSegment :: (Floating a, Ord a) => (a, a) -> (a, a) -> (a, a) -> a
distPointSegment (ax, ay) (bx, by) (px, py)
  | t <= 0 = sqrt (dx1 * dx1 + dy1 * dy1)
  | t >= 1 = sqrt (dx2 * dx2 + dy2 * dy2)
  | otherwise = sqrt (dx * dx + dy * dy)
  where
    abX = bx - ax
    abY = by - ay
    apX = px - ax
    apY = py - ay
    t = (apX * abX + apY * abY) / (abX * abX + abY * abY)
    projX = ax + t * abX
    projY = ay + t * abY
    dx = px - projX
    dy = py - projY
    dx1 = px - ax
    dy1 = py - ay
    dx2 = px - bx
    dy2 = py - by

-- | Rotate a point around the origin by an angle (in radians)
rotate :: (Floating a, Ord a) => a -> (a, a) -> (a, a)
rotate angle (x, y) =
  (x * cos angle - y * sin angle, x * sin angle + y * cos angle)

-- | A simple Rectangle type for geometry operations
--   Uses (x, y) as the top-left corner, with width and height
data GeometryRectangle a = GeometryRectangle
  { geoX :: !a,
    geoY :: !a,
    geoWidth :: !a,
    geoHeight :: !a
  }
  deriving (Eq, Show, Read)

-- | Convert X11's Rectangle to Geometry's Rectangle
rectangleToGeometry :: Num a => Graphics.X11.Rectangle -> GeometryRectangle a
rectangleToGeometry (Graphics.X11.Rectangle x y w h) =
  GeometryRectangle
    { geoX = fromIntegral x,
      geoY = fromIntegral y,
      geoWidth = fromIntegral w,
      geoHeight = fromIntegral h
    }

-- | Convert Geometry's Rectangle to X11's Rectangle
geometryToRectangle :: Integral a => GeometryRectangle a -> Graphics.X11.Rectangle
geometryToRectangle (GeometryRectangle x y w h) =
  Graphics.X11.Rectangle (fromIntegral x) (fromIntegral y) (fromIntegral w) (fromIntegral h)

-- | Alias for GeometryRectangle for backwards compatibility
type Rectangle a = GeometryRectangle a

-- | Result of intersecting a line with a rectangle
data IntersectionResult a b = IntersectionResult
  { tag :: a,
    rectangle :: GeometryRectangle b,
    intersectionPoint :: (b, b)
  }
  deriving (Eq, Show)

-- | Find all rectangles that intersect the given line, sorted by distance from origin
--   Returns tags of intersected rectangles in order of their intersection point
--   closest to (0,0)
getIntersectingRectangles :: (Floating a, Ord a) => Line a -> [(GeometryRectangle a, b)] -> [IntersectionResult b a]
getIntersectingRectangles line rects = catMaybes $ map (checkIntersection line) rects

-- | Check if a line intersects a rectangle, and if so, return the intersection point
--   closest to the origin
checkIntersection :: (Floating a, Ord a) => Line a -> (GeometryRectangle a, b) -> Maybe (IntersectionResult b a)
checkIntersection line (rect, tag) = IntersectionResult tag rect <$> findClosestIntersection line rect

-- | Find the intersection point closest to the origin
findClosestIntersection :: (Floating a, Ord a) => Line a -> GeometryRectangle a -> Maybe (a, a)
findClosestIntersection line rect =
  listToMaybe $ sortOn distanceFromOrigin $ filter (pointInRectangle rect) candidates
  where
    -- Find intersections with each edge of the rectangle
    topEdge = lineThrough2Points (geoX rect, geoY rect) (geoX rect + geoWidth rect, geoY rect)
    bottomEdge = lineThrough2Points (geoX rect, geoY rect + geoHeight rect) (geoX rect + geoWidth rect, geoY rect + geoHeight rect)
    leftEdge = lineThrough2Points (geoX rect, geoY rect) (geoX rect, geoY rect + geoHeight rect)
    rightEdge = lineThrough2Points (geoX rect + geoWidth rect, geoY rect) (geoX rect + geoWidth rect, geoY rect + geoHeight rect)

    candidates =
      catMaybes
        [ intersectLines line topEdge,
          intersectLines line bottomEdge,
          intersectLines line leftEdge,
          intersectLines line rightEdge
        ]

-- | Check if a point is inside a rectangle (including edges)
pointInRectangle :: (Num a, Ord a) => GeometryRectangle a -> (a, a) -> Bool
pointInRectangle rect (px, py) =
  px >= geoX rect
    && px <= geoX rect + geoWidth rect
    && py >= geoY rect
    && py <= geoY rect + geoHeight rect

-- | Find the intersection point of two lines, if it exists
intersectLines :: (Fractional a, Eq a) => Line a -> Line a -> Maybe (a, a)
intersectLines (Line x1 y1 dx1 dy1) (Line x2 y2 dx2 dy2) =
  -- Solve: x1 + t*dx1 = x2 + s*dx2 and y1 + t*dy1 = y2 + s*dy2
  -- Using Cramer's rule
  let det = dx1 * (- dy2) - dy1 * (- dx2)
      detT = (x2 - x1) * (- dy2) - (y2 - y1) * (- dx2)
   in if det == 0
        then Nothing -- Parallel lines
        else
          let t = detT / det
              intersectionX = x1 + t * dx1
              intersectionY = y1 + t * dy1
           in Just (intersectionX, intersectionY)

-- | Euclidean distance from origin
distanceFromOrigin :: (Num a, Floating a) => (a, a) -> a
distanceFromOrigin (x, y) = sqrt (x * x + y * y)