z_mirror/flib

git clone https://git.raiguard.me/z_mirror/flib

Caleb HeuerRemove require guardsa9f2be7

trunk
9.6 KiB333 linesraw
1local flib_position = require("__flib__.position")
2
3--- Utilities for manipulating bounding boxes. All functions support both the shorthand and explicit syntaxes for boxes
4--- and positions, and will preserve the syntax that was passed in. Boxes are considered immutable; all functions will
5--- return new boxes.
6--- ```lua
7--- local flib_bounding_box = require("__flib__.bounding-box")
8--- ```
9--- @class flib_bounding_box
10local flib_bounding_box = {}
11
12--- Return a new box expanded to the nearest tile edges.
13--- @param box BoundingBox
14--- @return BoundingBox
15function flib_bounding_box.ceil(box)
16  if box.left_top then
17    return {
18      left_top = { x = math.floor(box.left_top.x), y = math.floor(box.left_top.y) },
19      right_bottom = { x = math.ceil(box.right_bottom.x), y = math.ceil(box.right_bottom.y) },
20    }
21  else
22    return {
23      { math.floor(box[1][1]), math.floor(box[1][2]) },
24      { math.ceil(box[2][1]), math.ceil(box[2][2]) },
25    }
26  end
27end
28
29--- Calculate the centerpoint of the box.
30--- @param box BoundingBox
31--- @return MapPosition
32function flib_bounding_box.center(box)
33  if box.left_top then
34    return {
35      x = (box.left_top.x + box.right_bottom.x) / 2,
36      y = (box.left_top.y + box.right_bottom.y) / 2,
37    }
38  else
39    return {
40      (box[1][1] + box[2][1]) / 2,
41      (box[1][2] + box[2][2]) / 2,
42    }
43  end
44end
45
46--- Check if the first box contains the second box.
47--- @param box1 BoundingBox
48--- @param box2 BoundingBox
49--- @return boolean
50function flib_bounding_box.contains_box(box1, box2)
51  local box1 = flib_bounding_box.ensure_explicit(box1)
52  local box2 = flib_bounding_box.ensure_explicit(box2)
53
54  return box1.left_top.x <= box2.left_top.x
55    and box1.left_top.y <= box2.left_top.y
56    and box1.right_bottom.x >= box2.right_bottom.x
57    and box1.right_bottom.y >= box2.right_bottom.y
58end
59
60--- Check if the given box contains the given position.
61--- @param box BoundingBox
62--- @param pos MapPosition
63--- @return boolean
64function flib_bounding_box.contains_position(box, pos)
65  local box = flib_bounding_box.ensure_explicit(box)
66  local pos = flib_position.ensure_explicit(pos)
67  return box.left_top.x <= pos.x
68    and box.left_top.y <= pos.y
69    and box.right_bottom.x >= pos.x
70    and box.right_bottom.y >= pos.y
71end
72
73--- Return the box in explicit form.
74--- @param box BoundingBox
75--- @return BoundingBox
76function flib_bounding_box.ensure_explicit(box)
77  return {
78    left_top = flib_position.ensure_explicit(box.left_top or box[1]),
79    right_bottom = flib_position.ensure_explicit(box.right_bottom or box[2]),
80  }
81end
82
83--- Return the box in shorthand form.
84--- @param box BoundingBox
85--- @return BoundingBox
86function flib_bounding_box.ensure_short(box)
87  return {
88    flib_position.ensure_short(box.left_top or box[1]),
89    flib_position.ensure_short(box.right_bottom or box[2]),
90  }
91end
92
93--- Return a new box with initial dimensions box1, expanded to contain box2.
94--- @param box1 BoundingBox
95--- @param box2 BoundingBox
96--- @return BoundingBox
97function flib_bounding_box.expand_to_contain_box(box1, box2)
98  local box2 = flib_bounding_box.ensure_explicit(box2)
99
100  if box1.left_top then
101    return {
102      left_top = {
103        x = math.min(box1.left_top.x, box2.left_top.x),
104        y = math.min(box1.left_top.y, box2.left_top.y),
105      },
106      right_bottom = {
107        x = math.max(box1.right_bottom.x, box2.right_bottom.x),
108        y = math.max(box1.right_bottom.y, box2.right_bottom.y),
109      },
110    }
111  else
112    return {
113      {
114        math.min(box1[1][1], box2.left_top.x),
115        math.min(box1[1][2], box2.left_top.y),
116      },
117      {
118        math.max(box1[2][1], box2.right_bottom.x),
119        math.max(box1[2][2], box2.right_bottom.y),
120      },
121    }
122  end
123end
124
125--- Return a new box expanded to contain the given position.
126--- @param box BoundingBox
127--- @param pos MapPosition
128--- @return BoundingBox
129function flib_bounding_box.expand_to_contain_position(box, pos)
130  pos = flib_position.ensure_explicit(pos)
131
132  if box.left_top then
133    return {
134      left_top = { x = math.min(box.left_top.x, pos.x), y = math.min(box.left_top.y, pos.y) },
135      right_bottom = { x = math.max(box.right_bottom.x, pos.x), y = math.max(box.right_bottom.y, pos.y) },
136    }
137  else
138    return {
139      { math.min(box[1][1], pos.x), math.min(box[1][2], pos.y) },
140      { math.max(box[2][1], pos.x), math.max(box[2][2], pos.y) },
141    }
142  end
143end
144
145--- Return a new box shrunk to the nearest tile edges.
146--- @param box BoundingBox
147--- @return BoundingBox
148function flib_bounding_box.floor(box)
149  if box.left_top then
150    return {
151      left_top = { x = math.ceil(box.left_top.x), y = math.ceil(box.left_top.y) },
152      right_bottom = { x = math.floor(box.right_bottom.x), y = math.floor(box.right_bottom.y) },
153    }
154  else
155    return {
156      { math.ceil(box[1][1]), math.ceil(box[1][2]) },
157      { math.floor(box[2][1]), math.floor(box[2][2]) },
158    }
159  end
160end
161
162--- Create a new box from a centerpoint and dimensions.
163--- @param center MapPosition
164--- @param width number
165--- @param height number
166--- @return BoundingBox
167function flib_bounding_box.from_dimensions(center, width, height)
168  if center.x then
169    return {
170      left_top = { x = center.x - width / 2, y = center.y - height / 2 },
171      right_bottom = { x = center.x + width / 2, y = center.y + height / 2 },
172    }
173  else
174    return {
175      { center[1] - width / 2, center[2] - height / 2 },
176      { center[1] + width / 2, center[2] + height / 2 },
177    }
178  end
179end
180
181--- Create a 1x1 box from the given position, optionally snapped to the containing tile edges.
182--- @param pos MapPosition
183--- @param snap boolean?
184--- @return BoundingBox
185function flib_bounding_box.from_position(pos, snap)
186  if snap then
187    pos = flib_position.floor(pos)
188  else
189    pos = flib_position.sub(pos, { 0.5, 0.5 })
190  end
191  local x = pos.x or pos[1]
192  local y = pos.y or pos[2]
193  if pos.x then
194    return {
195      left_top = { x = x, y = y },
196      right_bottom = { x = x + 1, y = y + 1 },
197    }
198  else
199    return {
200      { x, y },
201      { x + 1, y + 1 },
202    }
203  end
204end
205
206--- Calculate the height of the box.
207--- @param box BoundingBox
208--- @return number
209function flib_bounding_box.height(box)
210  if box.left_top then
211    return box.right_bottom.y - box.left_top.y
212  else
213    return box[2][2] - box[1][2]
214  end
215end
216
217--- Check if the first box intersects (overlaps) the second box.
218--- @param box1 BoundingBox
219--- @param box2 BoundingBox
220--- @return boolean
221function flib_bounding_box.intersects_box(box1, box2)
222  local box1 = flib_bounding_box.ensure_explicit(box1)
223  local box2 = flib_bounding_box.ensure_explicit(box2)
224  return box1.left_top.x < box2.right_bottom.x
225    and box2.left_top.x < box1.right_bottom.x
226    and box1.left_top.y < box2.right_bottom.y
227    and box2.left_top.y < box1.right_bottom.y
228end
229
230--- Return a new box with  the same dimensions, moved by the given delta.
231--- @param box BoundingBox
232--- @param delta MapPosition
233--- @return BoundingBox
234function flib_bounding_box.move(box, delta)
235  local dx = delta.x or delta[1]
236  local dy = delta.y or delta[2]
237  if box.left_top then
238    return {
239      left_top = { x = box.left_top.x + dx, y = box.left_top.y + dy },
240      right_bottom = { x = box.right_bottom.x + dx, y = box.right_bottom.y + dy },
241    }
242  else
243    return {
244      { box[1][1] + dx, box[1][2] + dy },
245      { box[2][1] + dx, box[2][2] + dy },
246    }
247  end
248end
249
250--- Return a new box with the same dimensions centered on the given position.
251--- @param box BoundingBox
252--- @param pos MapPosition
253--- @return BoundingBox
254function flib_bounding_box.recenter_on(box, pos)
255  local height = flib_bounding_box.height(box)
256  local width = flib_bounding_box.width(box)
257
258  local pos_x = pos.x or pos[1]
259  local pos_y = pos.y or pos[2]
260
261  if box.left_top then
262    return {
263      left_top = { x = pos_x - (width / 2), y = pos_y - (height / 2) },
264      right_bottom = { x = pos_x + (width / 2), y = pos_y + (height / 2) },
265    }
266  else
267    return {
268      { pos_x - (width / 2), pos_y - (height / 2) },
269      { pos_x + (width / 2), pos_y + (height / 2) },
270    }
271  end
272end
273
274--- Return a new box grown or shrunk by the given delta. A positive delta will grow the box, a negative delta will
275--- shrink it.
276--- @param box BoundingBox
277--- @param delta number
278--- @return BoundingBox
279function flib_bounding_box.resize(box, delta)
280  if box.left_top then
281    return {
282      left_top = { x = box.left_top.x - delta, y = box.left_top.y - delta },
283      right_bottom = { x = box.right_bottom.x + delta, y = box.right_bottom.y + delta },
284    }
285  else
286    return {
287      { box[1][1] - delta, box[1][2] - delta },
288      { box[2][1] + delta, box[2][2] + delta },
289    }
290  end
291end
292
293--- Return a new box rotated 90 degrees about its center.
294--- @param box BoundingBox
295--- @return BoundingBox
296function flib_bounding_box.rotate(box)
297  local center = flib_bounding_box.center(box)
298  local radius_x = flib_bounding_box.width(box) / 2
299  local radius_y = flib_bounding_box.height(box) / 2
300
301  if box.left_top then
302    return {
303      left_top = { x = center.x - radius_y, y = center.y - radius_x },
304      right_bottom = { x = center.x + radius_y, y = center.y + radius_x },
305    }
306  else
307    return {
308      { center.x - radius_y, center.y - radius_x },
309      { center.x + radius_y, center.y + radius_x },
310    }
311  end
312end
313
314--- Return a new box expanded to create a square.
315--- @param box BoundingBox
316--- @return BoundingBox
317function flib_bounding_box.square(box)
318  local radius = math.max(flib_bounding_box.width(box), flib_bounding_box.height(box))
319  return flib_bounding_box.from_dimensions(flib_bounding_box.center(box), radius, radius)
320end
321
322--- Calculate the width of the box.
323--- @param box BoundingBox
324--- @return number
325function flib_bounding_box.width(box)
326  if box.left_top then
327    return box.right_bottom.x - box.left_top.x
328  else
329    return box[2][1] - box[1][1]
330  end
331end
332
333return flib_bounding_box