gifenc 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/geometry.rb CHANGED
@@ -2,8 +2,472 @@ module Gifenc
2
2
  # This module encapsulates all the necessary geometric functionality, and
3
3
  # more generally, all mathematical methods that may be useful for several
4
4
  # tasks of the library, such as drawing, resampling, etc.
5
+ #
6
+ # Every method that takes a point as argument may be supplied by providing
7
+ # either a `Point` or a `[Float, Float]` array representing its coordinates,
8
+ # regardless of whether the documentation has one or the other in the method's
9
+ # specification.
5
10
  module Geometry
6
11
 
12
+ # Represents a point in the plane. It's essentially a wrapper for an Float
13
+ # array with 2 elements (the coordinates) and many geometric methods that
14
+ # aid working with them. It is used indistinctly for both points and vectors,
15
+ # and will be denoted as such throughout the code, depending on which
16
+ # interpretation is more relevant.
17
+ class Point
18
+
19
+ # The X coordinate of the point.
20
+ # @return [Integer] X coordinate.
21
+ attr_accessor :x
22
+
23
+ # The Y coordinate of the point.
24
+ # @return [Integer] Y coordinate.
25
+ attr_accessor :y
26
+
27
+ # Convert polar coordinates to rectangular (Cartesian) coordinates.
28
+ # @param mod [Float] The point's module (euclidean norm).
29
+ # @param arg [Float] The point's argument (angle with respect to the
30
+ # positive X axis).
31
+ # @return [Array<Float>] The corresponding Cartesian coordinates.
32
+ def self.polar2rect(mod, arg)
33
+ [mod * Math.cos(arg), mod * Math.sin(arg)]
34
+ end
35
+
36
+ # Convert rectangular (Cartesian) coordinates to polar coordinates.
37
+ # @param x [Float] The point's X coordinate.
38
+ # @param y [Float] The point's Y coordinate.
39
+ # @return [Array<Float>] The corresponding polar coordinates.
40
+ def self.rect2polar(x, y)
41
+ [(x ** 2 + y ** 2) ** 0.5, Math.atan2(y, x)]
42
+ end
43
+
44
+ # Parse a point from an arbitrary argument. It accepts either:
45
+ # * A point object, in which case it returns itself.
46
+ # * An array, in which case it creates a new point whose coordinates are
47
+ # the values of the array.
48
+ # @param point [Point,Array<Integer>] The parameter to parse the point from.
49
+ # @param sys [Symbol] The coordinate system to use for parsing the
50
+ # coordinates. It may be `:cartesian` or `:polar`.
51
+ # @return [Point] The parsed point object.
52
+ # @raise [Exception::GeometryError] When a point couldn't be parsed from the supplied
53
+ # argument.
54
+ def self.parse(point, sys = :cartesian)
55
+ if point.is_a?(Point)
56
+ point
57
+ elsif point.is_a?(Array)
58
+ point = polar2rect(*point) if sys == :polar
59
+ new(*point)
60
+ else
61
+ raise Exception::GeometryError, "Couldn't parse point from argument."
62
+ end
63
+ end
64
+
65
+ # Create a new point given its coordinates. The coordinates are assumed to
66
+ # be the Cartesian coordinates with respect to the same axes as the desired
67
+ # image, and they need not be integers (though they'll be casted as such
68
+ # when actually drawing them).
69
+ # @param x [Float] The X coordinate of the point.
70
+ # @param y [Float] The Y coordinate of the point.
71
+ def initialize(x, y)
72
+ @x = x.to_f
73
+ @y = y.to_f
74
+ end
75
+
76
+ # Add another point to this one.
77
+ # @param p [Point] The other point.
78
+ # @return [Point] The new point.
79
+ def +(p)
80
+ p = Point.parse(p)
81
+ Point.new(@x + p.x, @y + p.y)
82
+ end
83
+
84
+ # Make all coordinates positive. This is equivalent to reflecting the
85
+ # point about the coordinate axes until it is in the first quadrant.
86
+ # @return (see #+)
87
+ def +@
88
+ Point.new(@x.abs, @y.abs)
89
+ end
90
+
91
+ # Subtract another point to this one.
92
+ # @param (see #+)
93
+ # @return (see #+)
94
+ def -(p)
95
+ p = Point.parse(p)
96
+ Point.new(@x - p.x, @y - p.y)
97
+ end
98
+
99
+ # Take the opposite point with respect to the origin. This is equivalent
100
+ # to performing half a rotation about the origin.
101
+ # @return (see #-)
102
+ def -@
103
+ Point.new(-@x, -@y)
104
+ end
105
+
106
+ # Scale a point or compute the dot product of two points.
107
+ # * If `arg` is Numeric, the point will be scaled by that factor. The
108
+ # return value will then be a new Point.
109
+ # * If `arg` is a Point, the scalar product of the two points will be
110
+ # computed. The return value will then be a Float.
111
+ # @param arg [Numeric,Point] The factor to scale the point.
112
+ # @return [Point,Float] The scaled point or the scalar product.
113
+ def *(arg)
114
+ if Numeric === arg
115
+ Point.new(@x * arg, @y * arg)
116
+ else
117
+ p = Point.parse(arg)
118
+ @x * p.x + @y * p.y
119
+ end
120
+ end
121
+
122
+ # Scale the point by the inverse of a factor.
123
+ # @param (see #*)
124
+ # @return (see #+)
125
+ def /(s)
126
+ Point.new(@x / s, @y / s)
127
+ end
128
+
129
+ # Project the point onto the given vector. The supplied vector need not be
130
+ # unitary, as it will be normalized automatically.
131
+ # @param (see #+)
132
+ # @return [Point] The new projected point.
133
+ def |(p)
134
+ u = Point.parse(p).normalize
135
+ u * (self * u)
136
+ end
137
+
138
+ # Reflect the point with respect to the given one.
139
+ # @param (see #+)
140
+ # @return [Point] The new reflected point.
141
+ def ^(p)
142
+ self + (Point.parse(p) - self) * 2
143
+ end
144
+
145
+ # Compute the midpoint between this point and the given one.
146
+ # @param (see #+)
147
+ # @return [Point] The midpoint.
148
+ def &(p)
149
+ (self + Point.parse(p)) / 2
150
+ end
151
+
152
+ # Return the standard (Cartesian) coordinates of the point. This consists
153
+ # on the X and Y values.
154
+ # @return [Array<Float>] Cartesian coordinates of the point.
155
+ def coords_cartesian
156
+ [@x, @y]
157
+ end
158
+
159
+ alias_method :coords, :coords_cartesian
160
+
161
+ # Return the polar coordinates of the point. This consists on the module
162
+ # (euclidean norm) and argument (angle between -PI and PI).
163
+ # @return [Array<Float>] Polar coordinates of the point.
164
+ def coords_polar
165
+ [mod, arg]
166
+ end
167
+
168
+ alias_method :polar, :coords_polar
169
+
170
+ # Compute the left-hand (CCW) normal vector.
171
+ # @return [Point] The left-hand normal vector.
172
+ # @see #normal_right
173
+ def normal_left
174
+ Point.new(@y, -@x)
175
+ end
176
+
177
+ # Compute the right-hand (CW) normal vector.
178
+ # @return [Point] The right-hand normal vector.
179
+ # @see #normal_left
180
+ def normal_right
181
+ Point.new(-@y, @x)
182
+ end
183
+
184
+ alias_method :normal, :normal_right
185
+
186
+ # Compute the p-norm of the vector. It should be `p>0`.
187
+ # @param p [Float] The parameter of the norm.
188
+ # @return [Float] The p-norm of the vector.
189
+ # @see #norm_1
190
+ # @see #norm
191
+ # @see #norm_inf
192
+ def norm_p(p = 2)
193
+ (@x.abs ** p + @y.abs ** p) ** (1.0 / p)
194
+ end
195
+
196
+ # Shortcut to compute the 1-norm of the vector.
197
+ # @return [Float] The 1-norm of the vector.
198
+ # @see #norm_p
199
+ def norm_1
200
+ (@x.abs + @y.abs).to_f
201
+ end
202
+
203
+ # Shortcut to compute the euclidean norm of the vector.
204
+ # @return [Float] The euclidean norm of the vector.
205
+ # @see #norm_p
206
+ def norm
207
+ norm_p(2)
208
+ end
209
+
210
+ alias_method :norm_2, :norm
211
+ alias_method :mod, :norm
212
+
213
+ # Shortcut to compute the infinity (maximum) norm of the vector.
214
+ # @return [Float] The infinity norm of the vector.
215
+ # @see #norm_p
216
+ def norm_inf
217
+ [@x.abs, @y.abs].max.to_f
218
+ end
219
+
220
+ # Normalize the vector with respect to the p-norm. It should be `p>0`.
221
+ # @param (see #norm_p)
222
+ # @return [Point] The normalized vector.
223
+ # @see #normalize_1
224
+ # @see #normalize
225
+ # @see #normalize_inf
226
+ # @raise [Exception::GeometryError] If trying to normalize the null vector.
227
+ def normalize_p(p)
228
+ normalize_gen(norm_p(p))
229
+ end
230
+
231
+ # Shotcut to normalize the vector with respect to the 1-norm.
232
+ # @return (see #normalize_p)
233
+ # @see #normalize_p
234
+ # @raise [Exception::GeometryError] If trying to normalize the null vector.
235
+ def normalize_1
236
+ normalize_p(1)
237
+ end
238
+
239
+ # Shotcut to normalize the vector with respect to the euclidean norm.
240
+ # @return (see #normalize_p)
241
+ # @see #normalize_p
242
+ # @raise [Exception::GeometryError] If trying to normalize the null vector.
243
+ def normalize
244
+ normalize_p(2)
245
+ end
246
+
247
+ alias_method :normalize_2, :normalize
248
+
249
+ # Shotcut to normalize the vector with respect to the infinity norm.
250
+ # @return (see #normalize_p)
251
+ # @see #normalize_p
252
+ # @raise [Exception::GeometryError] If trying to normalize the null vector.
253
+ def normalize_inf
254
+ normalize_gen(norm_inf)
255
+ end
256
+
257
+ # Compute the Euclidean distance between two points.
258
+ # @param (see #+)
259
+ # @return
260
+ def distance(p)
261
+ (self - Point.parse(p)).norm
262
+ end
263
+
264
+ # Project the point onto a line. The line might be supplied by providing
265
+ # either of the following 3 options:
266
+ # * Two different points from the line.
267
+ # * A point and a direction vector (not necessarily normalized).
268
+ # * A point and an angle.
269
+ # At least one point is therefore always required.
270
+ # @param p1 [Point] A point on the line.
271
+ # @param p2 [Point] Another point on the line.
272
+ # @param direction [Point] The direction vector of the line.
273
+ # @param angle [Float] The angle of the line, in radians.
274
+ # @return [Point] The projected point on the line.
275
+ # @raise [Exception::GeometryError] If the line couldn't be determined
276
+ # from the supplied arguments.
277
+ def project(p1: nil, p2: nil, direction: nil, angle: nil)
278
+ raise Exception::GeometryError, "Couldn't determine line,\
279
+ at least one point must be supplied." if !p1 && !p2
280
+ point = Point.parse(p1 || p2)
281
+ direction = Geometry.direction(p1: p1, p2: p2, angle: angle) unless direction
282
+ point - ((point - self) | direction)
283
+ end
284
+
285
+ # Reflect the point with respect to a line. The line might be supplied by
286
+ # providing either of the following 3 options:
287
+ # * Two different points from the line.
288
+ # * A point and a direction vector (not necessarily normalized).
289
+ # * A point and an angle.
290
+ # At least one point is therefore always required.
291
+ # @param t [Float] Proportion of reflection to perform. For example:
292
+ # * `t = 1` will perform the full reflection.
293
+ # * `t = 0` will land on the line.
294
+ # * `t = -1` will not move the point.
295
+ # @param (see #project)
296
+ # @return [Point] The reflected point with respect to the line.
297
+ # @raise (see #project)
298
+ def reflect(t = 1, p1: nil, p2: nil, direction: nil, angle: nil)
299
+ proj = self.project(p1: p1, p2: p2, direction: direction, angle: angle)
300
+ self + (proj - self) * (t + 1)
301
+ end
302
+
303
+ # Return the angle (argument) of the point. It is expressed in radian,
304
+ # between -PI and PI.
305
+ # @return [Float] Angle of the point.
306
+ def arg
307
+ Math.atan2(@y, @x)
308
+ end
309
+
310
+ # Whether the point is null, i.e., close enough to the origin.
311
+ # @return [Boolean] Whether the point is (close enough to) the origin.
312
+ def zero?
313
+ norm < PRECISION
314
+ end
315
+
316
+ # Find the angle between this point and the given one. The angle will be
317
+ # in the interval [0, PI].
318
+ # @param (see #+)
319
+ # @return [Float] Angle between the points.
320
+ def angle(p)
321
+ p = Point.parse(p)
322
+ Math.acos((self * p) / (norm * p.norm))
323
+ end
324
+
325
+ # Find whether the given point is perpendicular to this one.
326
+ # @param (see #+)
327
+ # @return [Boolean] Whether the points are orthogonal.
328
+ def orthogonal?(p)
329
+ (self * Point.parse(p)).abs < PRECISION
330
+ end
331
+
332
+ alias_method :perpendicular?, :orthogonal?
333
+
334
+ # Find whether the given point / vector is parallel (proportional) to
335
+ # this one.
336
+ # @param (see #+)
337
+ # @return [Boolean] Whether the points are parallel.
338
+ def parallel?(p)
339
+ angle(p).abs < PRECISION
340
+ end
341
+
342
+ # Find whether the points are positively aligned. This means that their
343
+ # scalar product is positive, and implies that they form an acute angle,
344
+ # i.e., they go roughly in the same direction.
345
+ # @param (see #+)
346
+ # @return [Boolean] Whether the points are positively aligned.
347
+ def positive?(p)
348
+ self * p > 0
349
+ end
350
+
351
+ # Find whether the points are negatively aligned. This means that their
352
+ # scalar product is negative, and implies that they form an obtuse angle,
353
+ # i.e., they go roughly in the opposite direction.
354
+ # @param (see #+)
355
+ # @return [Boolean] Whether the points are negatively aligned.
356
+ def negative?(p)
357
+ self * p < 0
358
+ end
359
+
360
+ # Rotate the point by a certain angle about a given center.
361
+ # @param angle [Float] The angle to rotate the point, in radians.
362
+ # @param center [Point] The point to rotate about.
363
+ # @return (see #+)
364
+ def rotate(angle, center = ORIGIN)
365
+ center = Point.parse(center)
366
+ x_old = @x - center.x
367
+ y_old = @y - center.y
368
+ sin = Math.sin(angle)
369
+ cos = Math.cos(angle)
370
+ x = x_old * cos - y_old * sin
371
+ y = x_old * sin + y_old * cos
372
+ Point.new(x + center.x, y + center.y)
373
+ end
374
+
375
+ # Shortcut to rotate the point 90 degrees counterclockwise about a given
376
+ # center.
377
+ # @param center [Point] The point to rotate about.
378
+ # @return (see #+)
379
+ def rotate_left(center = ORIGIN)
380
+ rotate(-Math::PI / 2, center)
381
+ end
382
+
383
+ # Shortcut to rotate the point 90 degrees clockwise about a given center.
384
+ # @param (see #rotate_left)
385
+ # @return (see #+)
386
+ def rotate_right(center = ORIGIN)
387
+ rotate(Math::PI / 2, center)
388
+ end
389
+
390
+ # Shortcut to rotate the point 180 degrees about a given center.
391
+ # @param (see #rotate_left)
392
+ # @return (see #+)
393
+ def rotate_180(center = ORIGIN)
394
+ rotate(Math::PI, center)
395
+ end
396
+
397
+ alias_method :translate, :+
398
+ alias_method :scale, :*
399
+
400
+ # Convert to integer point by rounding the coordinates.
401
+ # @return (see #+)
402
+ # @see #to_i
403
+ # @see #floor
404
+ # @see #ceil
405
+ def round
406
+ Point.new(@x.round, @y.round)
407
+ end
408
+
409
+ # Convert to integer point by taking the integer part of the coordinates.
410
+ # @return (see #+)
411
+ # @see #floor
412
+ # @see #ceil
413
+ # @see #round
414
+ def to_i
415
+ Point.new(@x.to_i, @y.to_i)
416
+ end
417
+
418
+ alias_method :truncate, :to_i
419
+
420
+ # Convert to integer point by taking the floor part of the coordinates.
421
+ # @return (see #+)
422
+ # @see #to_i
423
+ # @see #ceil
424
+ # @see #round
425
+ def floor
426
+ Point.new(@x.floor, @y.floor)
427
+ end
428
+
429
+ # Convert to integer point by taking the ceiling part of the coordinates.
430
+ # @return (see #+)
431
+ # @see #to_i
432
+ # @see #floor
433
+ # @see #round
434
+ def ceil
435
+ Point.new(@x.ceil, @y.ceil)
436
+ end
437
+
438
+ # Format the point's coordinates in the usual form.
439
+ # @return [String] The formatted point.
440
+ def to_s
441
+ "(#{@x}, #{@y})"
442
+ end
443
+
444
+ private
445
+
446
+ # Normalize the vector with respect to an arbitrary norm.
447
+ def normalize_gen(norm)
448
+ raise Exception::GeometryError, "Cannot normalize null vector." if zero?
449
+ Point.new(@x / norm, @y / norm)
450
+ end
451
+
452
+ end # Class Point
453
+
454
+ # Precision of the floating point math. Anything below this threshold will
455
+ # be considered 0.
456
+ # @return [Float] Floating point math precision.
457
+ PRECISION = 1E-7
458
+
459
+ # The point representing the origin of coordinates.
460
+ # @return [Point] Origin of coordinates.
461
+ ORIGIN = Point.new(0, 0)
462
+
463
+ # The point representing the first vector of the canonical base.
464
+ # @return [Point] First canonical vector.
465
+ E1 = Point.new(1, 0)
466
+
467
+ # The point representing the second vector of the canonical base.
468
+ # @return [Point] Second canonical vector.
469
+ E2 = Point.new(0, 1)
470
+
7
471
  # Finds the endpoint of a line given the startpoint and something else.
8
472
  # Namely, either of the following:
9
473
  # * The displacement vector (`vector`).
@@ -19,29 +483,44 @@ module Gifenc
19
483
  # @param length [Float] The length of the line. Must be provided if either
20
484
  # the `direction` or the `angle` method is being used.
21
485
  # @return [Array<Integer>] The [X, Y] coordinates of the line's endpoint.
22
- # @raise [Exception::CanvasError] If the supplied parameters don't suffice
486
+ # @raise [Exception::GeometryError] If the supplied parameters don't suffice
23
487
  # to determine a line (e.g. provided the `angle` but not the `length`).
24
488
  def self.endpoint(
25
489
  point: nil, vector: nil, direction: nil, angle: nil, length: nil
26
490
  )
27
- raise Exception::CanvasError, "The line start must be specified." if !point
491
+ raise Exception::GeometryError, "The line start must be specified." if !point
492
+ point = Point.parse(point)
28
493
  if vector
29
- x1 = x0 + vector[0]
30
- y1 = y0 + vector[1]
494
+ vector = Point.parse(vector)
495
+ x1 = point.x + vector.x
496
+ y1 = point.y + vector.y
31
497
  else
32
- raise Exception::CanvasError, "Either the endpoint, the vector or the length must be provided." if !length
498
+ raise Exception::GeometryError, "Either the endpoint, the vector or the length must be provided." if !length
33
499
  if direction
34
- mod = Math.sqrt(direction[0] ** 2 + direction[1] ** 2)
35
- direction[0] /= mod
36
- direction[1] /= mod
500
+ direction = Point.parse(direction).normalize
37
501
  else
38
- raise Exception::CanvasError, "The angle must be specified if no direction is provided." if !angle
39
- direction = [Math.cos(angle), Math.sin(angle)]
502
+ raise Exception::GeometryError, "The angle must be specified if no direction is provided." if !angle
503
+ direction = Point.new(Math.cos(angle), Math.sin(angle))
40
504
  end
41
- x1 = (point[0] + length * direction[0]).to_i
42
- y1 = (point[1] + length * direction[1]).to_i
505
+ x1 = (point.x + length * direction.x).to_i
506
+ y1 = (point.y + length * direction.y).to_i
43
507
  end
44
- [x1, y1]
508
+ Point.new(x1, y1)
509
+ end
510
+
511
+ # Find the unit direction vector of a line given either the endpoints or
512
+ # the angle.
513
+ # @param p1 [Point] One point of the line.
514
+ # @param p2 [Point] Another point of the line.
515
+ # @param angle [Float] The angle in radians.
516
+ # @return [Point] The unit direction vector.
517
+ # @raise [Exception::GeometryError] If not enough information is supplied
518
+ # (either the endpoints or the angle is required).
519
+ def self.direction(p1: nil, p2: nil, angle: nil)
520
+ return Point.parse([1, angle], :polar) if angle
521
+ raise Exception::GeometryError, "Couldn't parse direction, endpoints or|
522
+ angle must be supplied." if !p1 || !p2
523
+ (Point.parse(p1) - Point.parse(p2)).normalize
45
524
  end
46
525
 
47
526
  # Finds the bounding box of a set of points, i.e., the minimal rectangle
@@ -55,10 +534,11 @@ module Gifenc
55
534
  # `[X, Y]` are the coordinates of the upper left corner of the rectangle,
56
535
  # and `[W, H]` are its width and height, respectively.
57
536
  def self.bbox(points, pad = 0)
58
- x0 = points.min_by(&:first)[0] - pad
59
- y0 = points.min_by(&:last)[1] - pad
60
- x1 = points.max_by(&:first)[0] + pad
61
- y1 = points.max_by(&:last)[1] + pad
537
+ points = points.map{ |p| Point.parse(p) }
538
+ x0 = points.min_by(&:x).x.round - pad
539
+ y0 = points.min_by(&:y).y.round - pad
540
+ x1 = points.max_by(&:x).x.round + pad
541
+ y1 = points.max_by(&:y).y.round + pad
62
542
  [x0, y0, x1 - x0 + 1, y1 - y0 + 1]
63
543
  end
64
544
 
@@ -71,7 +551,8 @@ module Gifenc
71
551
  # the supplied points.
72
552
  # @return [Array<Array<Integer>>] The list of translated points.
73
553
  def self.translate(points, vector)
74
- points.map{ |p| [p[0] + vector[0], p[1] + vector[1]] }
554
+ vector = Point.parse(vector)
555
+ points.map{ |p| Point.parse(p) + vector }
75
556
  end
76
557
 
77
558
  # Computes the coordinates of a list of points relative to a provided bbox.
@@ -92,6 +573,43 @@ module Gifenc
92
573
  translate(points, [-bbox[0], -bbox[1]])
93
574
  end
94
575
 
576
+ # Compute the convex hull of a set of points. The convex hull is the smallest
577
+ # convex set containing the supplied points. This method will return the
578
+ # points located in the boundary of said hull in CCW order. The interior of
579
+ # the polygon they delimit is thus the convex hull.
580
+ #
581
+ # The *reduced* version will only include the extreme points of the boundary,
582
+ # i.e. the vertices, as opposed to all of them. The algorithm employed in
583
+ # both cases is the most basic one, known as the *Jarvis march*.
584
+ # @param points [Array<Point>] The list of points.
585
+ # @param reduced [Boolean] Whether to only return the vertices of the hull.
586
+ # @return [Array<Point>] The points composing the boundary of the convex hull.
587
+ def self.convex_hull(points, reduced = false)
588
+ points = points.uniq.map{ |p| Point.parse(p) }
589
+ return points if points.size < 3
590
+ hull_1 = points.min_by{ |p| [p.x, p.y] }
591
+ hull_2 = nil
592
+ hull_old = nil
593
+ hull = []
594
+ until hull_1 == hull.first
595
+ hull << hull_1
596
+ hull_2 = (points[0 ... 3] - [hull_1, hull_old]).first
597
+ points.each{ |p|
598
+ next if p == hull_1 || p == hull_2 || p == hull_old
599
+ index = cw(hull_1, p, hull_2)
600
+ next if index > PRECISION
601
+ if index.abs < PRECISION
602
+ d_new = hull_1.distance(p)
603
+ d_old = hull_1.distance(hull_2)
604
+ end
605
+ hull_2 = p if index < -PRECISION || (reduced ? d_new > d_old : d_new < d_old)
606
+ }
607
+ hull_old = hull_1
608
+ hull_1 = hull_2
609
+ end
610
+ hull
611
+ end
612
+
95
613
  # Checks if a list of points is entirely contained in the specified bounding
96
614
  # box.
97
615
  # @param (see #transform)
@@ -101,18 +619,76 @@ module Gifenc
101
619
  # @raise [Exception::CanvasError] If the points are not contained in the
102
620
  # bounding box and `silent` has not been set.
103
621
  def self.bound_check(points, bbox, silent = false)
622
+ bbox = [0, 0, bbox.width, bbox.height] if bbox.is_a?(Image)
623
+ points.map!{ |p| Point.parse(p) }
104
624
  outer_points = points.select{ |p|
105
- !p[0].between?(bbox[0], bbox[0] + bbox[2]) ||
106
- !p[1].between?(bbox[1], bbox[1] + bbox[3])
625
+ !p.x.between?(bbox[0], bbox[0] + bbox[2] - 1) ||
626
+ !p.y.between?(bbox[1], bbox[1] + bbox[3] - 1)
107
627
  }
108
628
  if outer_points.size > 0
109
629
  return false if silent
110
- points_str = outer_points.take(3).map{ |p| "(#{p[0]}, #{p[1]})" }
630
+ points_str = outer_points.take(3).map{ |p| "(#{p.x}, #{p.y})" }
111
631
  .join(', ') + '...'
112
632
  raise Exception::CanvasError, "Out of bounds pixels found: #{points_str}"
113
633
  end
114
634
  true
115
635
  end
116
636
 
117
- end
118
- end
637
+ # Compute a linear combination of points given the weights. The two supplied
638
+ # arrays should have the same length.
639
+ # @param points [Array<Point>] The points to combine.
640
+ # @param weights [Array<Float>] The weights to utilize for each point.
641
+ # @return [Point] The resulting linear combination.
642
+ # @raise [Exception::GeometryError] If the arrays' sizes differ.
643
+ def self.comb_linear(points, weights)
644
+ if points.size != weights.size
645
+ raise Exception::GeometryError, "Point and weight counts differ."
646
+ end
647
+ return ORIGIN if points.size == 0
648
+
649
+ points.map!{ |p| Point.parse(p) }
650
+ res = points[0] * weights[0]
651
+ points[1..-1].each_with_index{ |p, i|
652
+ res += p * weights[i + 1]
653
+ }
654
+ res
655
+ end
656
+
657
+ # Compute a convex combination of points given the weights. This is simply
658
+ # a linear combination, but the weights are normalized so they sum to 1.
659
+ # If they're positive, the resulting point will always be contained within
660
+ # the convex hull of the supplied points, hence the name. The two provided
661
+ # arrays should have the same length.
662
+ # @param (see #comb_linear)
663
+ # @return [Point] The resulting convex combination.
664
+ # @raise [Exception::GeometryError] If the arrays's sizes differ, or if the
665
+ # weights sum to 0.
666
+ def self.comb_convex(points, weights)
667
+ return ORIGIN if points.size == 0
668
+ weight = weights.sum
669
+ raise Exception::GeometryError, "Cannot find convex combination, weights sum to 0." if weight.abs < PRECISION
670
+ comb_linear(points, weights.map{ |w| w.to_f / weight })
671
+ end
672
+
673
+ # Find the center of mass (barycenter) of a list of points. This will always
674
+ # be contained within the convex hull of the supplied points.
675
+ # @param points [Array<Point>] The list of points.
676
+ # @return [Point]
677
+ def self.center(points)
678
+ raise Exception::GeometryError, "Cannot find center of empty list of points." if points.size == 0
679
+ points.map!{ |p| Point.parse(p) }
680
+ points.sum(ORIGIN) / points.size
681
+ end
682
+
683
+ private
684
+
685
+ # Index that indicates the clockwise order of 3 points:
686
+ # < 0 --> CCW
687
+ # = 0 --> Aligned
688
+ # > 0 --> CW
689
+ def self.cw(p, q, r)
690
+ (r.y - p.y) * (q.x - p.x) - (q.y - p.y) * (r.x - p.x)
691
+ end
692
+
693
+ end # Module Geometry
694
+ end # Module Gifenc