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,66 @@
|
|
1
|
+
require_relative './pointlike'
|
2
|
+
|
3
|
+
module Draught
|
4
|
+
# An abstract representation of a curve in a pointlike fashion, in the way
|
5
|
+
# a CubicBezier is pointlike
|
6
|
+
class Curve
|
7
|
+
include Pointlike
|
8
|
+
|
9
|
+
attr_reader :point
|
10
|
+
protected :point
|
11
|
+
|
12
|
+
def initialize(args = {})
|
13
|
+
@point = args.fetch(:point)
|
14
|
+
@cubic_beziers = args.fetch(:cubic_beziers).dup.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def x
|
18
|
+
@point.x
|
19
|
+
end
|
20
|
+
|
21
|
+
def y
|
22
|
+
@point.y
|
23
|
+
end
|
24
|
+
|
25
|
+
def point_type
|
26
|
+
:curve
|
27
|
+
end
|
28
|
+
|
29
|
+
def as_cubic_beziers
|
30
|
+
@cubic_beziers
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
other.point_type == point_type && other.point == point &&
|
35
|
+
other.as_cubic_beziers == as_cubic_beziers
|
36
|
+
end
|
37
|
+
|
38
|
+
def approximates?(other, delta)
|
39
|
+
other.point_type == point_type &&
|
40
|
+
point.approximates?(other.point, delta) &&
|
41
|
+
number_of_segments == other.number_of_segments &&
|
42
|
+
as_cubic_beziers.zip(other.as_cubic_beziers).all? { |a, b|
|
43
|
+
a.approximates?(b, delta)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def number_of_segments
|
48
|
+
@cubic_beziers.length
|
49
|
+
end
|
50
|
+
protected :number_of_segments
|
51
|
+
|
52
|
+
def translate(vector)
|
53
|
+
self.class.new({
|
54
|
+
point: @point.translate(vector),
|
55
|
+
cubic_beziers: @cubic_beziers.map { |c| c.translate(vector) }
|
56
|
+
})
|
57
|
+
end
|
58
|
+
|
59
|
+
def transform(transformer)
|
60
|
+
self.class.new({
|
61
|
+
point: @point.transform(transformer),
|
62
|
+
cubic_beziers: @cubic_beziers.map { |c| c.transform(transformer) }
|
63
|
+
})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/draught/line.rb
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
require_relative './path'
|
2
|
+
require_relative './pathlike'
|
3
|
+
require_relative './boxlike'
|
4
|
+
require_relative './point'
|
5
|
+
require_relative './transformations'
|
6
|
+
|
7
|
+
module Draught
|
8
|
+
class Line
|
9
|
+
DEGREES_90 = Math::PI / 2
|
10
|
+
DEGREES_180 = Math::PI
|
11
|
+
DEGREES_270 = Math::PI * 1.5
|
12
|
+
DEGREES_360 = Math::PI * 2
|
13
|
+
|
14
|
+
include Boxlike
|
15
|
+
include Pathlike
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def horizontal(width)
|
19
|
+
build(end_point: Point.new(width, 0))
|
20
|
+
end
|
21
|
+
|
22
|
+
def vertical(height)
|
23
|
+
build(end_point: Point.new(0, height))
|
24
|
+
end
|
25
|
+
|
26
|
+
def build(args = {})
|
27
|
+
builder_class = args.has_key?(:end_point) ? LineBuilderFromPoint : LineBuilderFromAngles
|
28
|
+
line_args = builder_class.new(args).line_args
|
29
|
+
new(line_args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def from_path(path)
|
33
|
+
if path.number_of_points != 2
|
34
|
+
raise ArgumentError, "path must contain exactly 2 points, this contained #{path.number_of_points}"
|
35
|
+
end
|
36
|
+
build(start_point: path.first, end_point: path.last)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :start_point, :end_point, :length, :radians
|
41
|
+
|
42
|
+
def initialize(args)
|
43
|
+
@start_point = args.fetch(:start_point, Point::ZERO)
|
44
|
+
@end_point = args.fetch(:end_point)
|
45
|
+
@length = args.fetch(:length)
|
46
|
+
@radians = args.fetch(:radians)
|
47
|
+
end
|
48
|
+
|
49
|
+
def points
|
50
|
+
@points ||= [start_point, end_point]
|
51
|
+
end
|
52
|
+
|
53
|
+
def extend(args = {})
|
54
|
+
default_args = {at: :end}
|
55
|
+
args = default_args.merge(args)
|
56
|
+
new_length = args[:to] || length + args[:by]
|
57
|
+
new_line = self.class.build({
|
58
|
+
start_point: start_point, length: new_length, radians: radians
|
59
|
+
})
|
60
|
+
args[:at] == :start ? shift_line(new_line) : new_line
|
61
|
+
end
|
62
|
+
|
63
|
+
def [](index_start_or_range, length = nil)
|
64
|
+
if length.nil?
|
65
|
+
case index_start_or_range
|
66
|
+
when Range
|
67
|
+
Path.new(points[index_start_or_range])
|
68
|
+
when Numeric
|
69
|
+
points[index_start_or_range]
|
70
|
+
else
|
71
|
+
raise TypeError, "requires a Range or Numeric in single-arg form"
|
72
|
+
end
|
73
|
+
else
|
74
|
+
Path.new(points[index_start_or_range, length])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def translate(vector)
|
79
|
+
self.class.build(Hash[
|
80
|
+
transform_args_hash.map { |arg, point| [arg, point.translate(vector)] }
|
81
|
+
])
|
82
|
+
end
|
83
|
+
|
84
|
+
def transform(transformation)
|
85
|
+
self.class.build(Hash[
|
86
|
+
transform_args_hash.map { |arg, point| [arg, point.transform(transformation)] }
|
87
|
+
])
|
88
|
+
end
|
89
|
+
|
90
|
+
def lower_left
|
91
|
+
@lower_left ||= Point.new(x_min, y_min)
|
92
|
+
end
|
93
|
+
|
94
|
+
def width
|
95
|
+
@width ||= x_max - x_min
|
96
|
+
end
|
97
|
+
|
98
|
+
def height
|
99
|
+
@height ||= y_max - y_min
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def shift_line(new_line)
|
105
|
+
translation = Vector.translation_between(new_line.end_point, end_point)
|
106
|
+
self.class.new({
|
107
|
+
start_point: start_point.translate(translation),
|
108
|
+
end_point: new_line.end_point.translate(translation),
|
109
|
+
length: new_line.length,
|
110
|
+
radians: radians
|
111
|
+
})
|
112
|
+
end
|
113
|
+
|
114
|
+
def transform_args_hash
|
115
|
+
{start_point: start_point, end_point: end_point}
|
116
|
+
end
|
117
|
+
|
118
|
+
def x_max
|
119
|
+
@x_max ||= points.map(&:x).max || 0
|
120
|
+
end
|
121
|
+
|
122
|
+
def x_min
|
123
|
+
@x_min ||= points.map(&:x).min || 0
|
124
|
+
end
|
125
|
+
|
126
|
+
def y_max
|
127
|
+
@y_max ||= points.map(&:y).max || 0
|
128
|
+
end
|
129
|
+
|
130
|
+
def y_min
|
131
|
+
@y_min ||= points.map(&:y).min || 0
|
132
|
+
end
|
133
|
+
|
134
|
+
class LineBuilderFromAngles
|
135
|
+
attr_reader :start_point, :length, :radians
|
136
|
+
private :start_point, :length, :radians
|
137
|
+
|
138
|
+
def initialize(args)
|
139
|
+
@start_point = args.fetch(:start_point, Point::ZERO)
|
140
|
+
@length = args.fetch(:length)
|
141
|
+
@radians = args.fetch(:radians)
|
142
|
+
end
|
143
|
+
|
144
|
+
def line_args
|
145
|
+
{length: length, radians: radians, start_point: start_point, end_point: end_point}
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def end_point
|
151
|
+
end_point_from_zero.translate(Vector.translation_between(Point::ZERO, start_point))
|
152
|
+
end
|
153
|
+
|
154
|
+
def end_point_from_zero
|
155
|
+
hardwired_end_points.fetch(restricted_radians) {
|
156
|
+
single_quadrant_end_point.transform(Transformations.rotate(remaining_angle))
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
def restricted_radians
|
161
|
+
@restricted_radians ||= restrict_to_360_degrees(radians)
|
162
|
+
end
|
163
|
+
|
164
|
+
def restrict_to_360_degrees(radians)
|
165
|
+
radians % DEGREES_360
|
166
|
+
end
|
167
|
+
|
168
|
+
def hardwired_end_points
|
169
|
+
{
|
170
|
+
0 => Point.new(length,0),
|
171
|
+
DEGREES_90 => Point.new(0,length),
|
172
|
+
DEGREES_180 => Point.new(-length,0),
|
173
|
+
DEGREES_270 => Point.new(0,-length),
|
174
|
+
DEGREES_360 => Point.new(length,0)
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def single_quadrant_end_point
|
179
|
+
Point.new(x, y)
|
180
|
+
end
|
181
|
+
|
182
|
+
def x
|
183
|
+
Math.cos(single_quadrant_angle) * length
|
184
|
+
end
|
185
|
+
|
186
|
+
def y
|
187
|
+
Math.sin(single_quadrant_angle) * length
|
188
|
+
end
|
189
|
+
|
190
|
+
def single_quadrant_angle
|
191
|
+
@single_quadrant_angle ||= restricted_radians - remaining_angle
|
192
|
+
end
|
193
|
+
|
194
|
+
def remaining_angle
|
195
|
+
@remaining_angle ||= begin
|
196
|
+
[DEGREES_270, DEGREES_180, DEGREES_90, 0].find { |angle|
|
197
|
+
restricted_radians > angle
|
198
|
+
} || 0
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class LineBuilderFromPoint
|
204
|
+
attr_reader :start_point, :end_point
|
205
|
+
private :start_point, :end_point
|
206
|
+
|
207
|
+
def initialize(args)
|
208
|
+
@start_point = args.fetch(:start_point, Point::ZERO)
|
209
|
+
@end_point = args.fetch(:end_point)
|
210
|
+
end
|
211
|
+
|
212
|
+
def line_args
|
213
|
+
{length: length, radians: radians, start_point: start_point, end_point: end_point}
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def end_point_from_zero
|
219
|
+
@end_point_from_zero ||= end_point.translate(Vector.translation_between(start_point, Point::ZERO))
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
def length
|
224
|
+
@length ||= begin
|
225
|
+
if x_length == 0 || y_length == 0
|
226
|
+
x_length + y_length
|
227
|
+
else
|
228
|
+
Math.sqrt(x_length ** 2 + y_length ** 2)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def radians
|
234
|
+
@radians ||= begin
|
235
|
+
if x_length == 0 || y_length == 0
|
236
|
+
angle_to_start_of_quadrant
|
237
|
+
else
|
238
|
+
angle_to_start_of_quadrant + angle_ignoring_quadrant
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def x_length
|
244
|
+
@x_length = end_point_from_zero.x.abs
|
245
|
+
end
|
246
|
+
|
247
|
+
def y_length
|
248
|
+
@y_length ||= end_point_from_zero.y.abs
|
249
|
+
end
|
250
|
+
|
251
|
+
def angle_to_start_of_quadrant
|
252
|
+
which_side_of_x = end_point_from_zero.x <=> 0
|
253
|
+
which_side_of_y = end_point_from_zero.y <=> 0
|
254
|
+
|
255
|
+
case [which_side_of_x, which_side_of_y]
|
256
|
+
when [1,0], [1, 1] # 0-90º
|
257
|
+
0
|
258
|
+
when [0,1], [-1, 1] # 90-180º
|
259
|
+
DEGREES_90
|
260
|
+
when [-1, 0], [-1, -1] # 180-270º
|
261
|
+
DEGREES_180
|
262
|
+
when [0, -1], [1, -1] # 270-360º
|
263
|
+
DEGREES_270
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def angle_ignoring_quadrant
|
268
|
+
Math.acos(y_length.to_f/length)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
data/lib/draught/path.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require_relative './boxlike'
|
3
|
+
require_relative './pathlike'
|
4
|
+
|
5
|
+
module Draught
|
6
|
+
class Path
|
7
|
+
include Boxlike
|
8
|
+
include Pathlike
|
9
|
+
|
10
|
+
attr_reader :points
|
11
|
+
|
12
|
+
def initialize(points = [])
|
13
|
+
@points = points.dup.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(point)
|
17
|
+
append(point)
|
18
|
+
end
|
19
|
+
|
20
|
+
def append(*paths_or_points)
|
21
|
+
paths_or_points.inject(self) { |path, point_or_path| path.add_points(point_or_path.points) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def prepend(*paths_or_points)
|
25
|
+
paths_or_points.inject(Path.new) { |path, point_or_path|
|
26
|
+
path.add_points(point_or_path.points)
|
27
|
+
}.add_points(self.points)
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](index_start_or_range, length = nil)
|
31
|
+
if length.nil?
|
32
|
+
case index_start_or_range
|
33
|
+
when Range
|
34
|
+
self.class.new(points[index_start_or_range])
|
35
|
+
when Numeric
|
36
|
+
points[index_start_or_range]
|
37
|
+
else
|
38
|
+
raise TypeError, "requires a Range or Numeric in single-arg form"
|
39
|
+
end
|
40
|
+
else
|
41
|
+
self.class.new(points[index_start_or_range, length])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def lower_left
|
46
|
+
@lower_left ||= Point.new(x_min, y_min)
|
47
|
+
end
|
48
|
+
|
49
|
+
def width
|
50
|
+
@width ||= x_max - x_min
|
51
|
+
end
|
52
|
+
|
53
|
+
def height
|
54
|
+
@height ||= y_max - y_min
|
55
|
+
end
|
56
|
+
|
57
|
+
def translate(vector)
|
58
|
+
self.class.new(points.map { |p| p.translate(vector) })
|
59
|
+
end
|
60
|
+
|
61
|
+
def transform(transformer)
|
62
|
+
self.class.new(points.map { |p| p.transform(transformer) })
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def add_points(points)
|
68
|
+
self.class.new(@points + points)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def x_max
|
74
|
+
@x_max ||= points.map(&:x).max || 0
|
75
|
+
end
|
76
|
+
|
77
|
+
def x_min
|
78
|
+
@x_min ||= points.map(&:x).min || 0
|
79
|
+
end
|
80
|
+
|
81
|
+
def y_max
|
82
|
+
@y_max ||= points.map(&:y).max || 0
|
83
|
+
end
|
84
|
+
|
85
|
+
def y_min
|
86
|
+
@y_min ||= points.map(&:y).min || 0
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'path'
|
2
|
+
|
3
|
+
module Draught
|
4
|
+
class PathBuilder
|
5
|
+
def self.build
|
6
|
+
builder = new
|
7
|
+
yield(builder)
|
8
|
+
builder.send(:path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.connect(*paths)
|
12
|
+
paths = paths.reject(&:empty?)
|
13
|
+
build { |p|
|
14
|
+
p << paths.shift
|
15
|
+
paths.inject(p.last) { |point, path|
|
16
|
+
translation = Vector.translation_between(path.first, point)
|
17
|
+
p << path.translate(translation)[1..-1]
|
18
|
+
p.last
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :path
|
24
|
+
private :path
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@path = Path.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def <<(path_or_point)
|
31
|
+
@path = path << path_or_point
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def last
|
36
|
+
path.last
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|