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.
@@ -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
@@ -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
@@ -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