draught 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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