Home

Awesome

Perfect Shape 1.0.8

Geometric Algorithms

Gem Version Test

(Used by Fukuoka 2022 Special Award Winning Glimmer DSL for LibUI Ruby Desktop Development GUI Library)

PerfectShape is a collection of pure Ruby geometric algorithms that are mostly useful for GUI (Graphical User Interface) manipulation like checking viewport rectangle intersection or containment of a mouse click point in popular geometry shapes such as rectangle, square, arc (open, chord, and pie), ellipse, circle, polygon, and paths containing lines, quadratic bézier curves, and cubic bezier curves, potentially with affine transforms applied like translation, scale, rotation, shear/skew, and inversion (including both the Ray Casting Algorithm, aka Even-odd Rule, and the Winding Number Algorithm, aka Nonzero Rule).

Additionally, PerfectShape::Math contains some purely mathematical algorithms, like IEEE 754-1985 Remainder.

To ensure accuracy and precision, this library does all its mathematical operations with BigDecimal numbers.

Setup

Run:

gem install perfect-shape -v 1.0.8

Or include in Bundler Gemfile:

gem 'perfect-shape', '~> 1.0.8'

And, run:

bundle

API

PerfectShape::Math

Module

PerfectShape::Shape

Class

This is a base class for all shapes. It is not meant to be used directly. Subclasses implement/override its methods as needed.

PerfectShape::PointLocation

Module

PerfectShape::RectangularShape

Module

Includes PerfectShape::PointLocation

PerfectShape::MultiPoint

Module

PerfectShape::AffineTransform

Class

Affine transforms have the following matrix:

[ xxp xyp xt ]<br> [ yxp yyp yt ]

The matrix is used to transform (x,y) point coordinates as follows:

[ xxp xyp xt ] * [x] = [ xxp * x + xyp * y + xt ]<br> [ yxp yyp yt ] * [y] = [ yxp * x + yyp * y + yt ]

xxp is the x coordinate x product (m11)<br> xyp is the x coordinate y product (m12)<br> yxp is the y coordinate x product (m21)<br> yyp is the y coordinate y product (m22)<br> xt is the x coordinate translation (m13)<br> yt is the y coordinate translation (m23)

Affine transform mutation operations ending with ! can be chained as they all return self.

Example:

xxp = 2
xyp = 3
yxp = 4
yyp = 5
xt = 6
yt = 7
affine_transform1 = PerfectShape::AffineTransform.new(xxp: xxp, xyp: xyp, yxp: yxp, yyp: yyp, xt: xt, yt: yt) # (x,y)-operation kwarg names
affine_transform2 = PerfectShape::AffineTransform.new(m11: xxp, m12: xyp, m21: yxp, m22: yyp, m13: xt, m23: yt) # traditional matrix element kwarg names
affine_transform3 = PerfectShape::AffineTransform.new(xxp, xyp, yxp, yyp, xt, yt) # standard arguments

affine_transform2.matrix_3d == affine_transform1.matrix_3d # => true
affine_transform3.matrix_3d == affine_transform1.matrix_3d # => true

affine_transform = PerfectShape::AffineTransform.new.translate!(30, 20).scale!(2, 3)

affine_transform.transform_point(10, 10) # => approximately [50, 50]
affine_transform.inverse_transform_point(50, 50) # => approximately [10, 10]

PerfectShape::Point

Class

Extends PerfectShape::Shape

Includes PerfectShape::PointLocation

point

Points are simply represented by an Array of [x,y] coordinates when used within other shapes, but when needing point-specific operations like point_distance, the PerfectShape::Point class can come in handy.

Example:

require 'perfect-shape'

shape = PerfectShape::Point.new(x: 200, y: 150)

shape.contain?(200, 150) # => true
shape.contain?([200, 150]) # => true
shape.contain?(200, 151) # => false
shape.contain?([200, 151]) # => false
shape.contain?(200, 151, distance_tolerance: 5) # => true
shape.contain?([200, 151], distance_tolerance: 5) # => true

PerfectShape::Line

Class

Extends PerfectShape::Shape

Includes PerfectShape::MultiPoint

line

Example:

require 'perfect-shape'

shape = PerfectShape::Line.new(points: [[0, 0], [100, 100]]) # start point and end point

shape.contain?(50, 50) # => true
shape.contain?([50, 50]) # => true
shape.contain?(50, 51) # => false
shape.contain?([50, 51]) # => false
shape.contain?(50, 51, distance_tolerance: 5) # => true
shape.contain?([50, 51], distance_tolerance: 5) # => true

PerfectShape::QuadraticBezierCurve

Class

Extends PerfectShape::Shape

Includes PerfectShape::MultiPoint

quadratic_bezier_curve

Example:

require 'perfect-shape'

shape = PerfectShape::QuadraticBezierCurve.new(points: [[200, 150], [270, 320], [380, 150]]) # start point, control point, and end point

shape.contain?(270, 220) # => true
shape.contain?([270, 220]) # => true
shape.contain?(270, 220, outline: true) # => false
shape.contain?([270, 220], outline: true) # => false
shape.contain?(280, 235, outline: true) # => true
shape.contain?([280, 235], outline: true) # => true
shape.contain?(281, 235, outline: true) # => false
shape.contain?([281, 235], outline: true) # => false
shape.contain?(281, 235, outline: true, distance_tolerance: 1) # => true
shape.contain?([281, 235], outline: true, distance_tolerance: 1) # => true

PerfectShape::CubicBezierCurve

Class

Extends PerfectShape::Shape

Includes PerfectShape::MultiPoint

cubic_bezier_curve

Example:

require 'perfect-shape'

shape = PerfectShape::CubicBezierCurve.new(points: [[200, 150], [235, 235], [270, 320], [380, 150]]) # start point, two control points, and end point

shape.contain?(270, 220) # => true
shape.contain?([270, 220]) # => true
shape.contain?(270, 220, outline: true) # => false
shape.contain?([270, 220], outline: true) # => false
shape.contain?(261.875, 245.625, outline: true) # => true
shape.contain?([261.875, 245.625], outline: true) # => true
shape.contain?(261.875, 246.625, outline: true) # => false
shape.contain?([261.875, 246.625], outline: true) # => false
shape.contain?(261.875, 246.625, outline: true, distance_tolerance: 1) # => true
shape.contain?([261.875, 246.625], outline: true, distance_tolerance: 1) # => true

PerfectShape::Rectangle

Class

Extends PerfectShape::Shape

Includes PerfectShape::RectangularShape

rectangle

Example:

require 'perfect-shape'

shape = PerfectShape::Rectangle.new(x: 15, y: 30, width: 200, height: 100)

shape.contain?(115, 80) # => true
shape.contain?([115, 80]) # => true
shape.contain?(115, 80, outline: true) # => false
shape.contain?([115, 80], outline: true) # => false
shape.contain?(115, 30, outline: true) # => true
shape.contain?([115, 30], outline: true) # => true
shape.contain?(115, 31, outline: true) # => false
shape.contain?([115, 31], outline: true) # => false
shape.contain?(115, 31, outline: true, distance_tolerance: 1) # => true
shape.contain?([115, 31], outline: true, distance_tolerance: 1) # => true

PerfectShape::Square

Class

Extends PerfectShape::Rectangle

square

Example:

require 'perfect-shape'

shape = PerfectShape::Square.new(x: 15, y: 30, length: 200)

shape.contain?(115, 130) # => true
shape.contain?([115, 130]) # => true
shape.contain?(115, 130, outline: true) # => false
shape.contain?([115, 130], outline: true) # => false
shape.contain?(115, 30, outline: true) # => true
shape.contain?([115, 30], outline: true) # => true
shape.contain?(115, 31, outline: true) # => false
shape.contain?([115, 31], outline: true) # => false
shape.contain?(115, 31, outline: true, distance_tolerance: 1) # => true
shape.contain?([115, 31], outline: true, distance_tolerance: 1) # => true

PerfectShape::Arc

Class

Extends PerfectShape::Shape

Includes PerfectShape::RectangularShape

Arcs can be of type :open, :chord, or :pie

Open ArcChord ArcPie Arc
arc-openarc-chordarc-pie

Example:

require 'perfect-shape'

shape = PerfectShape::Arc.new(type: :open, x: 2, y: 3, width: 50, height: 60, start: 45, extent: 270)
shape2 = PerfectShape::Arc.new(type: :open, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 45, extent: 270)

shape.contain?(39.5, 33.0) # => true
shape.contain?([39.5, 33.0]) # => true
shape2.contain?(39.5, 33.0) # => true
shape2.contain?([39.5, 33.0]) # => true
shape.contain?(39.5, 33.0, outline: true) # => false
shape.contain?([39.5, 33.0], outline: true) # => false
shape2.contain?(39.5, 33.0, outline: true) # => false
shape2.contain?([39.5, 33.0], outline: true) # => false
shape.contain?(2.0, 33.0, outline: true) # => true
shape.contain?([2.0, 33.0], outline: true) # => true
shape2.contain?(2.0, 33.0, outline: true) # => true
shape2.contain?([2.0, 33.0], outline: true) # => true
shape.contain?(3.0, 33.0, outline: true) # => false
shape.contain?([3.0, 33.0], outline: true) # => false
shape2.contain?(3.0, 33.0, outline: true) # => false
shape2.contain?([3.0, 33.0], outline: true) # => false
shape.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
shape.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
shape2.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
shape2.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
shape.contain?(shape.center_x, shape.center_y, outline: true) # => false
shape.contain?([shape.center_x, shape.center_y], outline: true) # => false
shape2.contain?(shape2.center_x, shape2.center_y, outline: true) # => false
shape2.contain?([shape2.center_x, shape2.center_y], outline: true) # => false

shape3 = PerfectShape::Arc.new(type: :chord, x: 2, y: 3, width: 50, height: 60, start: 45, extent: 270)
shape4 = PerfectShape::Arc.new(type: :chord, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 45, extent: 270)

shape3.contain?(39.5, 33.0) # => true
shape3.contain?([39.5, 33.0]) # => true
shape4.contain?(39.5, 33.0) # => true
shape4.contain?([39.5, 33.0]) # => true
shape3.contain?(39.5, 33.0, outline: true) # => false
shape3.contain?([39.5, 33.0], outline: true) # => false
shape4.contain?(39.5, 33.0, outline: true) # => false
shape4.contain?([39.5, 33.0], outline: true) # => false
shape3.contain?(2.0, 33.0, outline: true) # => true
shape3.contain?([2.0, 33.0], outline: true) # => true
shape4.contain?(2.0, 33.0, outline: true) # => true
shape4.contain?([2.0, 33.0], outline: true) # => true
shape3.contain?(3.0, 33.0, outline: true) # => false
shape3.contain?([3.0, 33.0], outline: true) # => false
shape4.contain?(3.0, 33.0, outline: true) # => false
shape4.contain?([3.0, 33.0], outline: true) # => false
shape3.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
shape3.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
shape4.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
shape4.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
shape3.contain?(shape3.center_x, shape3.center_y, outline: true) # => false
shape3.contain?([shape3.center_x, shape3.center_y], outline: true) # => false
shape4.contain?(shape4.center_x, shape4.center_y, outline: true) # => false
shape4.contain?([shape4.center_x, shape4.center_y], outline: true) # => false

shape5 = PerfectShape::Arc.new(type: :pie, x: 2, y: 3, width: 50, height: 60, start: 45, extent: 270)
shape6 = PerfectShape::Arc.new(type: :pie, center_x: 2 + 25, center_y: 3 + 30, radius_x: 25, radius_y: 30, start: 45, extent: 270)

shape5.contain?(39.5, 33.0) # => false
shape5.contain?([39.5, 33.0]) # => false
shape6.contain?(39.5, 33.0) # => false
shape6.contain?([39.5, 33.0]) # => false
shape5.contain?(9.5, 33.0) # => true
shape5.contain?([9.5, 33.0]) # => true
shape6.contain?(9.5, 33.0) # => true
shape6.contain?([9.5, 33.0]) # => true
shape5.contain?(39.5, 33.0, outline: true) # => false
shape5.contain?([39.5, 33.0], outline: true) # => false
shape6.contain?(39.5, 33.0, outline: true) # => false
shape6.contain?([39.5, 33.0], outline: true) # => false
shape5.contain?(2.0, 33.0, outline: true) # => true
shape5.contain?([2.0, 33.0], outline: true) # => true
shape6.contain?(2.0, 33.0, outline: true) # => true
shape6.contain?([2.0, 33.0], outline: true) # => true
shape5.contain?(3.0, 33.0, outline: true) # => false
shape5.contain?([3.0, 33.0], outline: true) # => false
shape6.contain?(3.0, 33.0, outline: true) # => false
shape6.contain?([3.0, 33.0], outline: true) # => false
shape5.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
shape5.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
shape6.contain?(3.0, 33.0, outline: true, distance_tolerance: 1.0) # => true
shape6.contain?([3.0, 33.0], outline: true, distance_tolerance: 1.0) # => true
shape5.contain?(shape5.center_x, shape5.center_y, outline: true) # => true
shape5.contain?([shape5.center_x, shape5.center_y], outline: true) # => true
shape6.contain?(shape6.center_x, shape6.center_y, outline: true) # => true
shape6.contain?([shape6.center_x, shape6.center_y], outline: true) # => true

PerfectShape::Ellipse

Class

Extends PerfectShape::Arc

ellipse

Example:

require 'perfect-shape'

shape = PerfectShape::Ellipse.new(x: 2, y: 3, width: 50, height: 60)
shape2 = PerfectShape::Ellipse.new(center_x: 27, center_y: 33, radius_x: 25, radius_y: 30)

shape.contain?(27, 33) # => true
shape.contain?([27, 33]) # => true
shape2.contain?(27, 33) # => true
shape2.contain?([27, 33]) # => true
shape.contain?(27, 33, outline: true) # => false
shape.contain?([27, 33], outline: true) # => false
shape2.contain?(27, 33, outline: true) # => false
shape2.contain?([27, 33], outline: true) # => false
shape.contain?(2, 33, outline: true) # => true
shape.contain?([2, 33], outline: true) # => true
shape2.contain?(2, 33, outline: true) # => true
shape2.contain?([2, 33], outline: true) # => true
shape.contain?(1, 33, outline: true) # => false
shape.contain?([1, 33], outline: true) # => false
shape2.contain?(1, 33, outline: true) # => false
shape2.contain?([1, 33], outline: true) # => false
shape.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
shape.contain?([1, 33], outline: true, distance_tolerance: 1) # => true
shape2.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
shape2.contain?([1, 33], outline: true, distance_tolerance: 1) # => true

PerfectShape::Circle

Class

Extends PerfectShape::Ellipse

circle

Example:

require 'perfect-shape'

shape = PerfectShape::Circle.new(x: 2, y: 3, diameter: 60)
shape2 = PerfectShape::Circle.new(center_x: 2 + 30, center_y: 3 + 30, radius: 30)

shape.contain?(32, 33) # => true
shape.contain?([32, 33]) # => true
shape2.contain?(32, 33) # => true
shape2.contain?([32, 33]) # => true
shape.contain?(32, 33, outline: true) # => false
shape.contain?([32, 33], outline: true) # => false
shape2.contain?(32, 33, outline: true) # => false
shape2.contain?([32, 33], outline: true) # => false
shape.contain?(2, 33, outline: true) # => true
shape.contain?([2, 33], outline: true) # => true
shape2.contain?(2, 33, outline: true) # => true
shape2.contain?([2, 33], outline: true) # => true
shape.contain?(1, 33, outline: true) # => false
shape.contain?([1, 33], outline: true) # => false
shape2.contain?(1, 33, outline: true) # => false
shape2.contain?([1, 33], outline: true) # => false
shape.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
shape.contain?([1, 33], outline: true, distance_tolerance: 1) # => true
shape2.contain?(1, 33, outline: true, distance_tolerance: 1) # => true
shape2.contain?([1, 33], outline: true, distance_tolerance: 1) # => true

PerfectShape::Polygon

Class

Extends PerfectShape::Shape

Includes PerfectShape::MultiPoint

A polygon can be thought of as a special case of path, consisting of lines only, is closed, and has the Even-Odd winding rule by default.

polygon

Example:

require 'perfect-shape'

shape = PerfectShape::Polygon.new(points: [[200, 150], [270, 170], [250, 220], [220, 190], [200, 200], [180, 170]])

shape.contain?(225, 185) # => true
shape.contain?([225, 185]) # => true
shape.contain?(225, 185, outline: true) # => false
shape.contain?([225, 185], outline: true) # => false
shape.contain?(200, 150, outline: true) # => true
shape.contain?([200, 150], outline: true) # => true
shape.contain?(200, 151, outline: true) # => false
shape.contain?([200, 151], outline: true) # => false
shape.contain?(200, 151, outline: true, distance_tolerance: 1) # => true
shape.contain?([200, 151], outline: true, distance_tolerance: 1) # => true

PerfectShape::Path

Class

Extends PerfectShape::Shape

Includes PerfectShape::MultiPoint

path

Example:

require 'perfect-shape'

path_shapes = []
path_shapes << PerfectShape::Point.new(x: 200, y: 150)
path_shapes << PerfectShape::Line.new(points: [250, 170]) # no need for start point, just end point
path_shapes << PerfectShape::QuadraticBezierCurve.new(points: [[300, 185], [350, 150]]) # no need for start point, just control point and end point
path_shapes << PerfectShape::CubicBezierCurve.new(points: [[370, 50], [430, 220], [480, 170]]) # no need for start point, just two control points and end point

shape = PerfectShape::Path.new(shapes: path_shapes, closed: false, winding_rule: :wind_non_zero)

shape.contain?(275, 165) # => true
shape.contain?([275, 165]) # => true
shape.contain?(275, 165, outline: true) # => false
shape.contain?([275, 165], outline: true) # => false
shape.contain?(shape.disconnected_shapes[1].curve_center_x, shape.disconnected_shapes[1].curve_center_y, outline: true) # => true
shape.contain?([shape.disconnected_shapes[1].curve_center_x, shape.disconnected_shapes[1].curve_center_y], outline: true) # => true
shape.contain?(shape.disconnected_shapes[1].curve_center_x + 1, shape.disconnected_shapes[1].curve_center_y, outline: true) # => false
shape.contain?([shape.disconnected_shapes[1].curve_center_x + 1, shape.disconnected_shapes[1].curve_center_y], outline: true) # => false
shape.contain?(shape.disconnected_shapes[1].curve_center_x + 1, shape.disconnected_shapes[1].curve_center_y, outline: true, distance_tolerance: 1) # => true
shape.contain?([shape.disconnected_shapes[1].curve_center_x + 1, shape.disconnected_shapes[1].curve_center_y], outline: true, distance_tolerance: 1) # => true

PerfectShape::CompositeShape

Class

Extends PerfectShape::Shape

A composite shape is simply an aggregate of multiple shapes (e.g. square and triangle polygon)

composite shape

Example:

require 'perfect-shape'

shapes = []
shapes << PerfectShape::Square.new(x: 120, y: 215, length: 100)
shapes << PerfectShape::Polygon.new(points: [[120, 215], [170, 165], [220, 215]])

shape = PerfectShape::CompositeShape.new(shapes: shapes)

shape.contain?(170, 265) # => true inside square
shape.contain?([170, 265]) # => true inside square
shape.contain?(170, 265, outline: true) # => false
shape.contain?([170, 265], outline: true) # => false
shape.contain?(170, 315, outline: true) # => true
shape.contain?([170, 315], outline: true) # => true
shape.contain?(170, 316, outline: true) # => false
shape.contain?([170, 316], outline: true) # => false
shape.contain?(170, 316, outline: true, distance_tolerance: 1) # => true
shape.contain?([170, 316], outline: true, distance_tolerance: 1) # => true

shape.contain?(170, 190) # => true inside polygon
shape.contain?([170, 190]) # => true inside polygon
shape.contain?(170, 190, outline: true) # => false
shape.contain?([170, 190], outline: true) # => false
shape.contain?(145, 190, outline: true) # => true
shape.contain?([145, 190], outline: true) # => true
shape.contain?(145, 189, outline: true) # => false
shape.contain?([145, 189], outline: true) # => false
shape.contain?(145, 189, outline: true, distance_tolerance: 1) # => true
shape.contain?([145, 189], outline: true, distance_tolerance: 1) # => true

Process

Glimmer Process

Resources

TODO

TODO.md

Change Log

CHANGELOG.md

Contributing

Copyright

MIT

Copyright (c) 2021-2022 Andy Maleh. See LICENSE.txt for further details.