draught 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/draught.gemspec +28 -0
- data/lib/draught.rb +4 -0
- data/lib/draught/arc_builder.rb +161 -0
- data/lib/draught/bounding_box.rb +76 -0
- data/lib/draught/boxlike.rb +148 -0
- data/lib/draught/container.rb +39 -0
- data/lib/draught/corner.rb +163 -0
- data/lib/draught/cubic_bezier.rb +62 -0
- data/lib/draught/curve.rb +66 -0
- data/lib/draught/line.rb +272 -0
- data/lib/draught/path.rb +89 -0
- data/lib/draught/path_builder.rb +39 -0
- data/lib/draught/path_cleaner.rb +77 -0
- data/lib/draught/pathlike.rb +57 -0
- data/lib/draught/point.rb +53 -0
- data/lib/draught/pointlike.rb +35 -0
- data/lib/draught/renderer.rb +93 -0
- data/lib/draught/sheet.rb +45 -0
- data/lib/draught/sheet_builder.rb +144 -0
- data/lib/draught/transformations.rb +63 -0
- data/lib/draught/transformations/affine.rb +33 -0
- data/lib/draught/transformations/common.rb +21 -0
- data/lib/draught/transformations/composer.rb +51 -0
- data/lib/draught/transformations/composition.rb +31 -0
- data/lib/draught/transformations/proclike.rb +43 -0
- data/lib/draught/vector.rb +48 -0
- data/lib/draught/version.rb +3 -0
- metadata +153 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative 'boxlike'
|
2
|
+
require_relative 'point'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
class BoundingBox
|
6
|
+
include Boxlike
|
7
|
+
|
8
|
+
attr_reader :paths
|
9
|
+
|
10
|
+
def initialize(*paths)
|
11
|
+
@paths = paths
|
12
|
+
end
|
13
|
+
|
14
|
+
def width
|
15
|
+
x_max - x_min
|
16
|
+
end
|
17
|
+
|
18
|
+
def height
|
19
|
+
y_max - y_min
|
20
|
+
end
|
21
|
+
|
22
|
+
def lower_left
|
23
|
+
@lower_left ||= Point.new(x_min, y_min)
|
24
|
+
end
|
25
|
+
|
26
|
+
def translate(point)
|
27
|
+
self.class.new(*paths.map { |path| path.translate(point) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def transform(transformer)
|
31
|
+
self.class.new(*paths.map { |path| path.transform(transformer) })
|
32
|
+
end
|
33
|
+
|
34
|
+
def zero_origin
|
35
|
+
move_to(Point::ZERO)
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
paths == other.paths
|
40
|
+
end
|
41
|
+
|
42
|
+
def containers
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
|
46
|
+
def box_type
|
47
|
+
[:container]
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def x_max
|
53
|
+
@x_max ||= upper_rights.map(&:x).max
|
54
|
+
end
|
55
|
+
|
56
|
+
def x_min
|
57
|
+
@x_min ||= lower_lefts.map(&:x).min
|
58
|
+
end
|
59
|
+
|
60
|
+
def y_max
|
61
|
+
@y_max ||= upper_rights.map(&:y).max
|
62
|
+
end
|
63
|
+
|
64
|
+
def y_min
|
65
|
+
@y_min ||= lower_lefts.map(&:y).min
|
66
|
+
end
|
67
|
+
|
68
|
+
def lower_lefts
|
69
|
+
@lower_lefts ||= paths.map(&:lower_left)
|
70
|
+
end
|
71
|
+
|
72
|
+
def upper_rights
|
73
|
+
@upper_rights ||= paths.map(&:upper_right)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative 'point'
|
2
|
+
require_relative 'vector'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
module Boxlike
|
6
|
+
POSITION_METHODS = [:lower_left, :lower_centre, :lower_right, :centre_right, :upper_right, :upper_centre, :upper_left, :centre_left, :centre]
|
7
|
+
|
8
|
+
def lower_left
|
9
|
+
raise NotImplementedError, "includers of Boxlike must implement #lower_left"
|
10
|
+
end
|
11
|
+
|
12
|
+
def width
|
13
|
+
raise NotImplementedError, "includers of Boxlike must implement #width"
|
14
|
+
end
|
15
|
+
|
16
|
+
def height
|
17
|
+
raise NotImplementedError, "includers of Boxlike must implement #height"
|
18
|
+
end
|
19
|
+
|
20
|
+
def box_type
|
21
|
+
raise NotImplementedError, "includers of Boxlike must implement #box_type"
|
22
|
+
end
|
23
|
+
|
24
|
+
def lower_right
|
25
|
+
@lower_right ||= lower_left.translate(Draught::Vector.new(width, 0))
|
26
|
+
end
|
27
|
+
|
28
|
+
def upper_right
|
29
|
+
@upper_right ||= lower_left.translate(Draught::Vector.new(width, height))
|
30
|
+
end
|
31
|
+
|
32
|
+
def upper_left
|
33
|
+
@upper_left ||= lower_left.translate(Draught::Vector.new(0, height))
|
34
|
+
end
|
35
|
+
|
36
|
+
def centre_left
|
37
|
+
@centre_left ||= lower_left.translate(Draught::Vector.new(0, height/2.0))
|
38
|
+
end
|
39
|
+
|
40
|
+
def lower_centre
|
41
|
+
@lower_centre ||= lower_left.translate(Draught::Vector.new(width/2.0, 0))
|
42
|
+
end
|
43
|
+
|
44
|
+
def centre_right
|
45
|
+
@centre_right ||= lower_right.translate(Draught::Vector.new(0, height / 2.0))
|
46
|
+
end
|
47
|
+
|
48
|
+
def upper_centre
|
49
|
+
@upper_centre ||= upper_left.translate(Draught::Vector.new(width/2.0, 0))
|
50
|
+
end
|
51
|
+
|
52
|
+
def centre
|
53
|
+
@centre ||= lower_left.translate(Draught::Vector.new(width/2.0, height/2.0))
|
54
|
+
end
|
55
|
+
|
56
|
+
def corners
|
57
|
+
[lower_left, lower_right, upper_right, upper_left]
|
58
|
+
end
|
59
|
+
|
60
|
+
def left_edge
|
61
|
+
@left_edge ||= lower_left.x
|
62
|
+
end
|
63
|
+
|
64
|
+
def right_edge
|
65
|
+
@right_edge ||= upper_right.x
|
66
|
+
end
|
67
|
+
|
68
|
+
def top_edge
|
69
|
+
@top_edge ||= upper_right.y
|
70
|
+
end
|
71
|
+
|
72
|
+
def bottom_edge
|
73
|
+
@bottom_edge ||= lower_left.y
|
74
|
+
end
|
75
|
+
|
76
|
+
def move_to(point, opts = {})
|
77
|
+
reference_position_method = opts.fetch(:position, :lower_left)
|
78
|
+
if invalid_position_method?(reference_position_method)
|
79
|
+
raise ArgumentError, ":position option must be a valid position (one of #{POSITION_METHODS.map(&:inspect).join(', ')}), rather than #{opts[:position].inspect}"
|
80
|
+
end
|
81
|
+
|
82
|
+
reference_point = send(reference_position_method)
|
83
|
+
translation = Draught::Vector.translation_between(reference_point, point)
|
84
|
+
return self if translation == Draught::Vector::NULL
|
85
|
+
translate(translation)
|
86
|
+
end
|
87
|
+
|
88
|
+
def translate(point)
|
89
|
+
raise NotImplementedError
|
90
|
+
end
|
91
|
+
|
92
|
+
def transform(transformer)
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
|
96
|
+
def paths
|
97
|
+
raise NotImplementedError
|
98
|
+
end
|
99
|
+
|
100
|
+
def containers
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
def overlaps?(other_box)
|
105
|
+
!disjoint?(other_box)
|
106
|
+
end
|
107
|
+
|
108
|
+
def disjoint?(other_box)
|
109
|
+
horizontal_disjoint?(other_box) || vertical_disjoint?(other_box)
|
110
|
+
end
|
111
|
+
|
112
|
+
def include_point?(point)
|
113
|
+
horizontal_extent.include?(point.x) && vertical_extent.include?(point.y)
|
114
|
+
end
|
115
|
+
|
116
|
+
def min_gap
|
117
|
+
0
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def horizontal_disjoint?(other_box)
|
123
|
+
other_box.left_edge == right_edge || other_box.right_edge == left_edge ||
|
124
|
+
other_box.left_edge > right_edge || other_box.right_edge < left_edge
|
125
|
+
end
|
126
|
+
|
127
|
+
def vertical_disjoint?(other_box)
|
128
|
+
other_box.bottom_edge == top_edge || other_box.top_edge == bottom_edge ||
|
129
|
+
other_box.top_edge < bottom_edge || other_box.bottom_edge > top_edge
|
130
|
+
end
|
131
|
+
|
132
|
+
def horizontal_extent
|
133
|
+
@horizontal_extent ||= lower_left.x..upper_right.x
|
134
|
+
end
|
135
|
+
|
136
|
+
def vertical_extent
|
137
|
+
@vertical_extent ||= lower_left.y..upper_right.y
|
138
|
+
end
|
139
|
+
|
140
|
+
def invalid_position_method?(method_name)
|
141
|
+
!valid_position_method?(method_name)
|
142
|
+
end
|
143
|
+
|
144
|
+
def valid_position_method?(method_name)
|
145
|
+
POSITION_METHODS.include?(method_name)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'boxlike'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
class Container
|
6
|
+
extend Forwardable
|
7
|
+
include Boxlike
|
8
|
+
|
9
|
+
attr_reader :box, :min_gap
|
10
|
+
|
11
|
+
def_delegators :box, :lower_left, :width, :height, :containers
|
12
|
+
|
13
|
+
def initialize(box, opts = {})
|
14
|
+
@box = box
|
15
|
+
@min_gap = opts.fetch(:min_gap, 0)
|
16
|
+
end
|
17
|
+
|
18
|
+
def translate(point)
|
19
|
+
self.class.new(box.translate(point), {min_gap: min_gap})
|
20
|
+
end
|
21
|
+
|
22
|
+
def transform(transformer)
|
23
|
+
transformed_min_gap = Point.new(min_gap,0).transform(transformer).x
|
24
|
+
self.class.new(box.transform(transformer), {min_gap: transformed_min_gap})
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
min_gap == other.min_gap && box == other.box
|
29
|
+
end
|
30
|
+
|
31
|
+
def box_type
|
32
|
+
[:container]
|
33
|
+
end
|
34
|
+
|
35
|
+
def paths
|
36
|
+
[box]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require_relative './path_builder'
|
2
|
+
require_relative './vector'
|
3
|
+
require_relative './line'
|
4
|
+
require_relative './arc_builder'
|
5
|
+
|
6
|
+
module Draught
|
7
|
+
class Corner
|
8
|
+
def self.join_rounded(args)
|
9
|
+
new(args).join
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :radius, :paths
|
13
|
+
|
14
|
+
def initialize(args)
|
15
|
+
@radius = args.fetch(:radius)
|
16
|
+
@paths = args.fetch(:paths)
|
17
|
+
end
|
18
|
+
|
19
|
+
def join
|
20
|
+
paths.inject { |incoming, outgoing|
|
21
|
+
Rounded.join(radius: radius, incoming: incoming, outgoing: outgoing)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
class Rounded
|
26
|
+
def self.join(args)
|
27
|
+
new(args).joined
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :radius, :incoming, :outgoing
|
31
|
+
|
32
|
+
def initialize(args)
|
33
|
+
@radius = args.fetch(:radius)
|
34
|
+
@incoming = args.fetch(:incoming)
|
35
|
+
@outgoing = args.fetch(:outgoing)
|
36
|
+
end
|
37
|
+
|
38
|
+
def joined
|
39
|
+
PathBuilder.connect(
|
40
|
+
incoming_before_final_segment,
|
41
|
+
incoming_final_segment,
|
42
|
+
corner_arc_path,
|
43
|
+
outgoing_first_segment,
|
44
|
+
outgoing_after_first_segment
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def incoming_before_final_segment
|
49
|
+
incoming[0..-2]
|
50
|
+
end
|
51
|
+
|
52
|
+
def incoming_final_segment
|
53
|
+
incoming_line.extend(by: -distance_to_tangent)
|
54
|
+
end
|
55
|
+
|
56
|
+
def incoming_line
|
57
|
+
Line.from_path(incoming[-2,2])
|
58
|
+
end
|
59
|
+
|
60
|
+
def outgoing_first_segment
|
61
|
+
outgoing_line.extend(by: -distance_to_tangent, at: :start)
|
62
|
+
end
|
63
|
+
|
64
|
+
def outgoing_line
|
65
|
+
Line.from_path(outgoing[0..1])
|
66
|
+
end
|
67
|
+
|
68
|
+
def outgoing_after_first_segment
|
69
|
+
outgoing[1..-1]
|
70
|
+
end
|
71
|
+
|
72
|
+
def distance_to_tangent
|
73
|
+
@distance_to_tangent ||= join_angles.tangent_distance(radius)
|
74
|
+
end
|
75
|
+
|
76
|
+
def join_angles
|
77
|
+
@join_angles ||= JoinAngles.new(incoming_line, outgoing_line)
|
78
|
+
end
|
79
|
+
|
80
|
+
def corner_arc_path
|
81
|
+
ArcBuilder.radians(angle: join_angles.arc_sweep, radius: radius, starting_angle: starting_angle).path
|
82
|
+
end
|
83
|
+
|
84
|
+
def starting_angle
|
85
|
+
incoming_line.radians - (Math::PI / 2)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class JoinAngles
|
90
|
+
attr_reader :incoming_line, :outgoing_line
|
91
|
+
|
92
|
+
def initialize(incoming_line, outgoing_line)
|
93
|
+
@incoming_line = incoming_line
|
94
|
+
@outgoing_line = outgoing_line
|
95
|
+
end
|
96
|
+
|
97
|
+
def arc_sweep
|
98
|
+
anticlockwise? ? abs_arc_sweep : abs_arc_sweep * -1
|
99
|
+
end
|
100
|
+
|
101
|
+
def tangent_distance(radius)
|
102
|
+
radius / Math.tan(abs_corner_angle / 2.0)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def abs_corner_angle
|
108
|
+
@abs_corner_angle ||= begin
|
109
|
+
half_corner_angle = Math.asin(corner_top_line.length/incoming_corner_line.length)
|
110
|
+
half_corner_angle * 2.0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def corner_top_line
|
115
|
+
@corner_top_line ||= begin
|
116
|
+
corner_top_line = Line.build({
|
117
|
+
start_point: incoming_corner_line.start_point, end_point: outgoing_corner_line.end_point
|
118
|
+
})
|
119
|
+
corner_top_line.extend(to: corner_top_line.length / 2.0)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def incoming_corner_line
|
124
|
+
@incoming_corner_line ||= zeroed_unit_line_segment(incoming_line, :last)
|
125
|
+
end
|
126
|
+
|
127
|
+
def outgoing_corner_line
|
128
|
+
@outgoing_corner_line ||= zeroed_unit_line_segment(outgoing_line, :first)
|
129
|
+
end
|
130
|
+
|
131
|
+
def zeroed_unit_line_segment(segment, zero_to_point)
|
132
|
+
unit_line_segment = segment.extend(to: 1)
|
133
|
+
unit_line_segment.translate(
|
134
|
+
Vector.translation_to_zero(unit_line_segment.public_send(zero_to_point))
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
def abs_arc_sweep
|
139
|
+
@abs_arc_sweep ||= Math::PI - abs_corner_angle
|
140
|
+
end
|
141
|
+
|
142
|
+
def anticlockwise?
|
143
|
+
!clockwise?
|
144
|
+
end
|
145
|
+
|
146
|
+
def clockwise?
|
147
|
+
if aligned_zeroed_lines[0].x > aligned_zeroed_lines[1].x
|
148
|
+
aligned_zeroed_lines[0].y < aligned_zeroed_lines[2].y
|
149
|
+
else
|
150
|
+
aligned_zeroed_lines[0].y > aligned_zeroed_lines[2].y
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def aligned_zeroed_lines
|
155
|
+
@aligned_zeroed_lines ||= begin
|
156
|
+
joined_lines = PathBuilder.connect(incoming_line, outgoing_line)
|
157
|
+
zeroed_joined_lines = joined_lines.translate(Vector.translation_to_zero(joined_lines.first))
|
158
|
+
zeroed_joined_lines.transform(Transformations.rotate(incoming_line.radians * -1))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative './pointlike'
|
2
|
+
|
3
|
+
module Draught
|
4
|
+
class CubicBezier
|
5
|
+
include Pointlike
|
6
|
+
|
7
|
+
attr_reader :end_point, :control_point_1, :control_point_2
|
8
|
+
|
9
|
+
def initialize(args = {})
|
10
|
+
@end_point = args.fetch(:end_point)
|
11
|
+
@control_point_1 = args.fetch(:control_point_1)
|
12
|
+
@control_point_2 = args.fetch(:control_point_2)
|
13
|
+
end
|
14
|
+
|
15
|
+
def x
|
16
|
+
end_point.x
|
17
|
+
end
|
18
|
+
|
19
|
+
def y
|
20
|
+
end_point.y
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
other.point_type == point_type &&
|
25
|
+
comparison_array(other).all? { |a, b| a == b }
|
26
|
+
end
|
27
|
+
|
28
|
+
def point_type
|
29
|
+
:cubic_bezier
|
30
|
+
end
|
31
|
+
|
32
|
+
def approximates?(other, delta)
|
33
|
+
other.point_type == point_type &&
|
34
|
+
comparison_array(other).all? { |a, b|
|
35
|
+
a.approximates?(b, delta)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def translate(vector)
|
40
|
+
transform(vector.to_transform)
|
41
|
+
end
|
42
|
+
|
43
|
+
def transform(transformer)
|
44
|
+
new_args = Hash[args_hash.map { |k, point|
|
45
|
+
[k, point.transform(transformer)]
|
46
|
+
}]
|
47
|
+
self.class.new(new_args)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def args_hash
|
53
|
+
{end_point: end_point, control_point_1: control_point_1, control_point_2: control_point_2}
|
54
|
+
end
|
55
|
+
|
56
|
+
def comparison_array(other)
|
57
|
+
args_hash.map { |arg, point|
|
58
|
+
[other.send(arg), point]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|