Module: Gifenc::Geometry
- Defined in:
- lib/geometry.rb
Overview
This module encapsulates all the necessary geometric functionality, and more generally, all mathematical methods that may be useful for several tasks of the library, such as drawing, resampling, etc.
Every method that takes a point as argument may be supplied by providing
either a Point
or a [Float, Float]
array representing its coordinates,
regardless of whether the documentation has one or the other in the method's
specification.
Defined Under Namespace
Classes: Point
Constant Summary collapse
- PRECISION =
Precision of the floating point math. Anything below this threshold will be considered 0.
1E-7
- ORIGIN =
The point representing the origin of coordinates.
Point.new(0, 0)
- E1 =
The point representing the first vector of the canonical base.
Point.new(1, 0)
- E2 =
The point representing the second vector of the canonical base.
Point.new(0, 1)
Class Method Summary collapse
-
.bbox(points, pad = 0) ⇒ Array<Integer>
Finds the bounding box of a set of points, i.e., the minimal rectangle containing all specified points.
-
.bound_check(points, bbox, silent = false) ⇒ Boolean
Checks if a list of points is entirely contained in the specified bounding box.
-
.center(points) ⇒ Point
Find the center of mass (barycenter) of a list of points.
-
.comb_convex(points, weights) ⇒ Point
Compute a convex combination of points given the weights.
-
.comb_linear(points, weights) ⇒ Point
Compute a linear combination of points given the weights.
-
.convex_hull(points, reduced = false) ⇒ Array<Point>
Compute the convex hull of a set of points.
-
.direction(p1: nil, p2: nil, angle: nil) ⇒ Point
Find the unit direction vector of a line given either the endpoints or the angle.
-
.endpoint(point: nil, vector: nil, direction: nil, angle: nil, length: nil) ⇒ Array<Integer>
Finds the endpoint of a line given the startpoint and something else.
-
.rect_overlap(*rects) ⇒ Array<Float>?
Calculate the intersection of multiple rectangles.
-
.transform(points, bbox) ⇒ Array<Array<Integer>>
Computes the coordinates of a list of points relative to a provided bbox.
-
.translate(points, vector) ⇒ Array<Array<Integer>>
Translate a set of points according to a fixed vector.
Class Method Details
.bbox(points, pad = 0) ⇒ Array<Integer>
Finds the bounding box of a set of points, i.e., the minimal rectangle containing all specified points.
536 537 538 539 540 541 542 543 |
# File 'lib/geometry.rb', line 536 def self.bbox(points, pad = 0) points = points.map{ |p| Point.parse(p) } x0 = points.min_by(&:x).x.round - pad y0 = points.min_by(&:y).y.round - pad x1 = points.max_by(&:x).x.round + pad y1 = points.max_by(&:y).y.round + pad [x0, y0, x1 - x0 + 1, y1 - y0 + 1] end |
.bound_check(points, bbox, silent = false) ⇒ Boolean
Checks if a list of points is entirely contained in the specified bounding box.
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 |
# File 'lib/geometry.rb', line 641 def self.bound_check(points, bbox, silent = false) bbox = [0, 0, bbox.width, bbox.height] if bbox.is_a?(Image) points.map!{ |p| Point.parse(p) } outer_points = points.select{ |p| !p.x.between?(bbox[0], bbox[0] + bbox[2] - 1) || !p.y.between?(bbox[1], bbox[1] + bbox[3] - 1) } if outer_points.size > 0 return false if silent points_str = outer_points.take(3).map{ |p| "(#{p.x}, #{p.y})" } .join(', ') + '...' raise Exception::CanvasError, "Out of bounds pixels found: #{points_str}" end true end |
.center(points) ⇒ Point
Find the center of mass (barycenter) of a list of points. This will always be contained within the convex hull of the supplied points.
698 699 700 701 702 |
# File 'lib/geometry.rb', line 698 def self.center(points) raise Exception::GeometryError, "Cannot find center of empty list of points." if points.size == 0 points.map!{ |p| Point.parse(p) } points.sum(ORIGIN) / points.size end |
.comb_convex(points, weights) ⇒ Point
Compute a convex combination of points given the weights. This is simply a linear combination, but the weights are normalized so they sum to 1. If they're positive, the resulting point will always be contained within the convex hull of the supplied points, hence the name. The two provided arrays should have the same length.
686 687 688 689 690 691 |
# File 'lib/geometry.rb', line 686 def self.comb_convex(points, weights) return ORIGIN if points.size == 0 weight = weights.sum raise Exception::GeometryError, "Cannot find convex combination, weights sum to 0." if weight.abs < PRECISION comb_linear(points, weights.map{ |w| w.to_f / weight }) end |
.comb_linear(points, weights) ⇒ Point
Compute a linear combination of points given the weights. The two supplied arrays should have the same length.
663 664 665 666 667 668 669 670 671 672 673 674 675 |
# File 'lib/geometry.rb', line 663 def self.comb_linear(points, weights) if points.size != weights.size raise Exception::GeometryError, "Point and weight counts differ." end return ORIGIN if points.size == 0 points.map!{ |p| Point.parse(p) } res = points[0] * weights[0] points[1..-1].each_with_index{ |p, i| res += p * weights[i + 1] } res end |
.convex_hull(points, reduced = false) ⇒ Array<Point>
Compute the convex hull of a set of points. The convex hull is the smallest convex set containing the supplied points. This method will return the points located in the boundary of said hull in CCW order. The interior of the polygon they delimit is thus the convex hull.
The reduced version will only include the extreme points of the boundary, i.e. the vertices, as opposed to all of them. The algorithm employed in both cases is the most basic one, known as the Jarvis march.
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'lib/geometry.rb', line 607 def self.convex_hull(points, reduced = false) points = points.uniq.map{ |p| Point.parse(p) } return points if points.size < 3 hull_1 = points.min_by{ |p| [p.x, p.y] } hull_2 = nil hull_old = nil hull = [] until hull_1 == hull.first hull << hull_1 hull_2 = (points[0 ... 3] - [hull_1, hull_old]).first points.each{ |p| next if p == hull_1 || p == hull_2 || p == hull_old index = cw(hull_1, p, hull_2) next if index > PRECISION if index.abs < PRECISION d_new = hull_1.distance(p) d_old = hull_1.distance(hull_2) end hull_2 = p if index < -PRECISION || (reduced ? d_new > d_old : d_new < d_old) } hull_old = hull_1 hull_1 = hull_2 end hull end |
.direction(p1: nil, p2: nil, angle: nil) ⇒ Point
Find the unit direction vector of a line given either the endpoints or the angle.
519 520 521 522 523 524 |
# File 'lib/geometry.rb', line 519 def self.direction(p1: nil, p2: nil, angle: nil) return Point.parse([1, angle], :polar) if angle raise Exception::GeometryError, "Couldn't parse direction, endpoints or| angle must be supplied." if !p1 || !p2 (Point.parse(p1) - Point.parse(p2)).normalize end |
.endpoint(point: nil, vector: nil, direction: nil, angle: nil, length: nil) ⇒ Array<Integer>
Finds the endpoint of a line given the startpoint and something else. Namely, either of the following:
- The displacement vector (
vector
). - The direction vector (
direction
) and the length (length
). - The angle (
angle
) and the length (length
).
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 |
# File 'lib/geometry.rb', line 488 def self.endpoint( point: nil, vector: nil, direction: nil, angle: nil, length: nil ) raise Exception::GeometryError, "The line start must be specified." if !point point = Point.parse(point) if vector vector = Point.parse(vector) x1 = point.x + vector.x y1 = point.y + vector.y else raise Exception::GeometryError, "Either the endpoint, the vector or the length must be provided." if !length if direction direction = Point.parse(direction).normalize else raise Exception::GeometryError, "The angle must be specified if no direction is provided." if !angle direction = Point.new(Math.cos(angle), Math.sin(angle)) end x1 = (point.x + length * direction.x).to_i y1 = (point.y + length * direction.y).to_i end Point.new(x1, y1) end |
.rect_overlap(*rects) ⇒ Array<Float>?
Calculate the intersection of multiple rectangles. The rectangles must be
supplied in the usual format of bounding boxes, i.e. [X, Y, W, H]
,
where X
and Y
are the coordinates of the upper left corner of the
rectangle with respect to the image, and W
and H
are the width and
height of the rectangle, respectively. The result will be a rectangle
in the same format, if the intersection is non-empty, or nil
otherwise.
553 554 555 556 557 558 559 560 561 562 563 |
# File 'lib/geometry.rb', line 553 def self.rect_overlap(*rects) return if rects.empty? rect = rects[0] rects[1..-1].each{ |r| rect = rect_overlap_two(rect, r) return if !rect } rect end |
.transform(points, bbox) ⇒ Array<Array<Integer>>
Computes the coordinates of a list of points relative to a provided bbox. This can be useful when we have the coordinates relative to the whole logical screen, but we want to use them in an image that doesn't cover the whole canvas. We can simply provide the bounding box of the image, that specifies the image's offset and dimensions, and this will transform the coordinates so that they can be used in the image with the same result. In practice, this simply translates the points by subtracting the upper left corner of the bounding box.
592 593 594 |
# File 'lib/geometry.rb', line 592 def self.transform(points, bbox) translate(points, [-bbox[0], -bbox[1]]) end |
.translate(points, vector) ⇒ Array<Array<Integer>>
Translate a set of points according to a fixed vector. Given a list of
points P1, ... , Pn
and a translation vector t
, this method will
return the transformed list of points P1 + t, ... , Pn + t
.
573 574 575 576 |
# File 'lib/geometry.rb', line 573 def self.translate(points, vector) vector = Point.parse(vector) points.map{ |p| Point.parse(p) + vector } end |