summaryrefslogtreecommitdiffhomepage
path: root/dragon/geometry.rb
blob: 6bceea8b0a69c3a361abe3cd2129d53da726b317 (plain)
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# coding: utf-8
# Copyright 2019 DragonRuby LLC
# MIT License
# geometry.rb has been released under MIT (*only this file*).

module GTK
  module Geometry
    def self.rotate_point point, angle, around = nil
      s = Math.sin a.to_radians
      c = Math.cos a.to_radians
      px = point.x
      py = point.y
      cx = 0
      cy = 0
      if around
        cx = around.x
        cy = around.y
      end

      point.merge(x: ((px - cx) * c - (py - cy) * s) + cx,
                  y: ((px - cx) * s + (py - cy) * c) + cy)
    end

    # Returns f(t) for a cubic Bezier curve.
    def self.cubic_bezier t, a, b, c, d
      s  = 1 - t
      s0 = 1
      s1 = s
      s2 = s * s
      s3 = s * s * s

      t0 = 1
      t1 = t
      t2 = t * t
      t3 = t * t * t

      1 * s3 * t0 * a +
      3 * s2 * t1 * b +
      3 * s1 * t2 * c +
      1 * s0 * t3 * d
    end

    # Returns true if a primitive's rectangle is entirely inside another primitive's rectangle.
    # @gtk
    def inside_rect? outer, tolerance = 0.0
      Geometry.inside_rect? self, outer, tolerance
    end

    # Returns true if a primitive's rectangle overlaps another primitive's rectangle.
    # @gtk
    def intersect_rect? other, tolerance = 0.1
      Geometry.intersect_rect? self, other, tolerance
    end

    def intersects_rect? *args
      Geometry.intersects_rect?(*args)
    end

    def scale_rect_extended percentage_x: percentage_x,
                            percentage_y: percentage_y,
                            anchor_x: anchor_x,
                            anchor_y: anchor_y

      Geometry.scale_rect_extended self,
                                   percentage_x: percentage_x,
                                   percentage_y: percentage_y,
                                   anchor_x: anchor_x,
                                   anchor_y: anchor_y
    end

    # Scales a primitive rect by a percentage.
    # @gtk
    def scale_rect percentage, *anchors
      Geometry.scale_rect self, percentage, *anchors
    end

    # Returns the angle from one primitive to another primitive.
    # @gtk
    def angle_to other_point
      Geometry.angle_to self, other_point
    end

    # Returns the angle to one primitive from another primitive.
    # @gtk
    def angle_from other_point
      Geometry.angle_from self, other_point
    end

    # Returns true if a primitive is within a circle specified by the circle's center and radius.
    # @gtk
    def point_inside_circle? circle_center_point, radius
      Geometry.point_inside_circle? self, circle_center_point, radius
    end

    def self.center_inside_rect rect, other_rect
      offset_x = (other_rect.w - rect.w).half
      offset_y = (other_rect.h - rect.h).half
      new_rect = rect.shift_rect(0, 0)
      new_rect.x = other_rect.x + offset_x
      new_rect.y = other_rect.y + offset_y
      new_rect
    rescue Exception => e
      raise e, <<-S
* ERROR:
center_inside_rect for self #{self} and other_rect #{other_rect}.\n#{e}.
S
    end

    def center_inside_rect other_rect
      Geometry.center_inside_rect self, other_rect
    end

    def self.center_inside_rect_x rect, other_rect
      offset_x   = (other_rect.w - rect.w).half
      new_rect   = rect.shift_rect(0, 0)
      new_rect.x = other_rect.x + offset_x
      new_rect.y = other_rect.y
      new_rect
    rescue Exception => e
      raise e, <<-S
* ERROR:
center_inside_rect_x for self #{self} and other_rect #{other_rect}.\n#{e}.
S
    end

    def center_inside_rect_x other_rect
      Geometry.center_inside_rect_x self, other_rect
    end

    def self.center_inside_rect_y rect, other_rect
      offset_y = (other_rect.h - rect.h).half
      new_rect = rect.shift_rect(0, 0)
      new_rect.x = other_rect.x
      new_rect.y = other_rect.y + offset_y
      new_rect
    rescue Exception => e
      raise e, <<-S
* ERROR:
center_inside_rect_y for self #{self} and other_rect #{other_rect}.\n#{e}.
S
    end

    def center_inside_rect_y other_rect
      Geometry.center_inside_rect_y self, other_rect
    end


    # Returns a primitive that is anchored/repositioned based off its rectangle.
    # @gtk
    def anchor_rect anchor_x, anchor_y
      current_w = self.w
      current_h = self.h
      delta_x = -1 * (anchor_x * current_w)
      delta_y = -1 * (anchor_y * current_h)
      self.shift_rect(delta_x, delta_y)
    end

    def angle_given_point other_point
      raise ":angle_given_point has been deprecated use :angle_from instead."
    end

    # @gtk
    def self.shift_line line, x, y
      if line.is_a?(Array) || line.is_a?(Hash)
        new_line = line.dup
        new_line.x  += x
        new_line.x2 += x
        new_line.y  += y
        new_line.y2 += y
        new_line
      else
        raise "shift_line for #{line} is not supported."
      end
    end

    def self.intersects_rect? *args
      raise <<-S
intersects_rect? (with an \"s\") has been deprecated.
Use intersect_rect? instead (remove the \"s\").

* NOTE:
Ruby's naming convention is to *never* include the \"s\" for
interrogative method names (methods that end with a ?). It
doesn't sound grammatically correct, but that has been the
rule for a long time (and why intersects_rect? has been deprecated).

S
    end

    # @gtk
    def self.line_y_intercept line
      line.y - line_slope(line) * line.x
    end

    # @gtk
    def self.angle_between_lines line_one, line_two, replace_infinity: nil
      m_line_one = line_slope line_one, replace_infinity: replace_infinity
      m_line_two = line_slope line_two, replace_infinity: replace_infinity
      Math.atan((m_line_one - m_line_two) / (1 + m_line_two * m_line_one)).to_degrees
    end

    # @gtk
    def self.line_slope line, replace_infinity: nil
      return replace_infinity if line.x2 == line.x
      (line.y2 - line.y).fdiv(line.x2 - line.x)
                        .replace_infinity(replace_infinity)
    end

    def self.line_rise_run line
      rise = (line.y2 - line.y).to_f
      run  = (line.x2 - line.x).to_f
      if rise.abs > run.abs && rise != 0
        rise = rise.fdiv rise.abs
        run = run.fdiv rise.abs
      elsif run.abs > rise.abs && run != 0
        rise = rise.fdiv run.abs
        run = run.fdiv run.abs
      else
        rise = rise / rise.abs if rise != 0
        run = run / run.abs if run != 0
      end
      return { x: run , y: rise }
    end

    # @gtk
    def self.ray_test point, line
      slope = (line.y2 - line.y).fdiv(line.x2 - line.x)

      if line.x > line.x2
        point_two, point_one = [point_one, point_two]
      end

      r = ((line.x2 - line.x) * (point.y - line.y) -
           (point.x -  line.x) * (line.y2 - line.y))

      if r == 0
        return :on
      elsif r < 0
        return :right if slope >= 0
        return :left
      elsif r > 0
        return :left if slope >= 0
        return :right
      end
    end

    # @gtk
    def self.line_rect line
      if line.x > line.x2
        x  = line.x2
        y  = line.y2
        x2 = line.x
        y2 = line.y
      else
        x  = line.x
        y  = line.y
        x2 = line.x2
        y2 = line.y2
      end

      w = x2 - x
      h = y2 - y

      { x: x, y: y, w: w, h: h }
    end

    # @gtk
    def self.line_intersect line_one, line_two
      m1 = line_slope(line_one)
      m2 = line_slope(line_two)
      b1 = line_y_intercept(line_one)
      b2 = line_y_intercept(line_two)
      x = (b1 - b2) / (m2 - m1)
      y = (-b2.fdiv(m2) + b1.fdiv(m1)).fdiv(1.fdiv(m1) - 1.fdiv(m2))
      [x, y]
    end

    def self.contract_intersect_rect?
      [:left, :right, :top, :bottom]
    end

    # @gtk
    def self.intersect_rect? rect_one, rect_two, tolerance = 0.1
      return false if ((rect_one.x + rect_one.w) - tolerance) < (rect_two.x + tolerance)
      return false if (rect_one.x + tolerance) > ((rect_two.x + rect_two.w) - tolerance)
      return false if ((rect_one.y + rect_one.h) - tolerance) < (rect_two.y + tolerance)
      return false if (rect_one.y + tolerance) > ((rect_two.y + rect_two.h) - tolerance)
      return true
    rescue Exception => e
      context_help_rect_one = (rect_one.__help_contract_implementation contract_intersect_rect?)[:not_implemented_methods]
      context_help_rect_two = (rect_two.__help_contract_implementation contract_intersect_rect?)[:not_implemented_methods]
      context_help = ""
      if context_help_rect_one && context_help_rect_one.length > 0
        context_help += <<-S
rect_one needs to implement the following methods: #{context_help_rect_one}

You may want to try include the ~AttrRect~ module which will give you these methods.
S
      end

      if context_help_rect_two && context_help_rect_two.length > 0
        context_help += <<-S
* FAILURE REASON:
rect_two needs to implement the following methods: #{context_help_rect_two}
NOTE: You may want to try include the ~GTK::Geometry~ module which will give you these methods.
S
      end

      raise e, <<-S
* ERROR:
:intersect_rect? failed for
- rect_one: #{rect_one}
- rect_two: #{rect_two}
#{context_help}
\n#{e}
S
    end

    # @gtk
    def self.to_square size, x, y, anchor_x = 0.5, anchor_y = nil
      anchor_y ||= anchor_x
      x = x.shift_left(size * anchor_x)
      y = y.shift_down(size * anchor_y)
      [x, y, size, size]
    rescue Exception => e
      raise e, ":to_square failed for size: #{size} x: #{x} y: #{y} anchor_x: #{anchor_x} anchor_y: #{anchor_y}.\n#{e}"
    end

    # @gtk
    def self.distance point_one, point_two
      Math.sqrt((point_two.x - point_one.x)**2 + (point_two.y - point_one.y)**2)
    rescue Exception => e
      raise e, ":distance failed for point_one: #{point_one} point_two #{point_two}.\n#{e}"
    end

    # @gtk
    def self.angle_from start_point, end_point
      d_y = end_point.y - start_point.y
      d_x = end_point.x - start_point.x
      Math::PI.+(Math.atan2(d_y, d_x)).to_degrees
    rescue Exception => e
      raise e, ":angle_from failed for start_point: #{start_point} end_point: #{end_point}.\n#{e}"
    end

    # @gtk
    def self.angle_to start_point, end_point
      angle_from end_point, start_point
    rescue Exception => e
      raise e, ":angle_to failed for start_point: #{start_point} end_point: #{end_point}.\n#{e}"
    end

    # @gtk
    def self.point_inside_circle? point, circle_center_point, radius
      (point.x - circle_center_point.x) ** 2 + (point.y - circle_center_point.y) ** 2 < radius ** 2
    rescue Exception => e
      raise e, ":point_inside_circle? failed for point: #{point} circle_center_point: #{circle_center_point} radius: #{radius}.\n#{e}"
    end

    # @gtk
    def self.inside_rect? inner_rect, outer_rect, tolerance = 0.0
      return nil if !inner_rect
      return nil if !outer_rect

      inner_rect.x     + tolerance >= outer_rect.x     - tolerance &&
      (inner_rect.x + inner_rect.w) - tolerance <= (outer_rect.x + outer_rect.w) + tolerance &&
      inner_rect.y     + tolerance >= outer_rect.y     - tolerance &&
      (inner_rect.y + inner_rect.h) - tolerance <= (outer_rect.y + outer_rect.h) + tolerance
    rescue Exception => e
      raise e, ":inside_rect? failed for inner_rect: #{inner_rect} outer_rect: #{outer_rect}.\n#{e}"
    end

    # @gtk
    def self.scale_rect_extended rect,
                                 percentage_x: percentage_x,
                                 percentage_y: percentage_y,
                                 anchor_x: anchor_x,
                                 anchor_y: anchor_y
      anchor_x ||= 0.0
      anchor_y ||= 0.0
      percentage_x ||= 1.0
      percentage_y ||= 1.0
      new_w = rect.w * percentage_x
      new_h = rect.h * percentage_y
      new_x = rect.x + (rect.w - new_w) * anchor_x
      new_y = rect.y + (rect.h - new_h) * anchor_y
      if rect.is_a? Array
        return [
          new_x,
          new_y,
          new_w,
          new_h,
          *rect[4..-1]
        ]
      elsif rect.is_a? Hash
        return rect.merge(x: new_x, y: new_y, w: new_w, h: new_h)
      else
        rect.x = new_x
        rect.y = new_y
        rect.w = new_w
        rect.h = new_h
        return rect
      end
    rescue Exception => e
      raise e, ":scale_rect_extended failed for rect: #{rect} percentage_x: #{percentage_x} percentage_y: #{percentage_y} anchors_x: #{anchor_x} anchor_y: #{anchor_y}.\n#{e}"
    end

    # @gtk
    def self.scale_rect rect, percentage, *anchors
      anchor_x, anchor_y = *anchors.flatten
      anchor_x ||= 0
      anchor_y ||= anchor_x
      Geometry.scale_rect_extended rect,
                                   percentage_x: percentage,
                                   percentage_y: percentage,
                                   anchor_x: anchor_x,
                                   anchor_y: anchor_y
    rescue Exception => e
      raise e, ":scale_rect failed for rect: #{rect} percentage: #{percentage} anchors [#{anchor_x} (x), #{anchor_y} (y)].\n#{e}"
    end

    def self.rect_to_line rect
      l = rect.to_hash.line
      l.merge(x2: l.x + l.w - 1,
              y2: l.y + l.h)
    end

    def self.rect_center_point rect
      { x: rect.x + rect.w.half, y: rect.y + rect.h.half }
    end

    def rect_center_point
      Geometry.rect_center_point self
    end
  end # module Geometry
end # module GTK