sscharter 0.9.0 → 0.10.1
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 +4 -4
- data/Gemfile.lock +1 -0
- data/Rakefile +8 -2
- data/lib/sscharter/chart.rb +52 -8
- data/lib/sscharter/charter/basic_events.rb +277 -0
- data/lib/sscharter/charter/beat.rb +334 -0
- data/lib/sscharter/charter/check.rb +37 -0
- data/lib/sscharter/charter/event.rb +429 -0
- data/lib/sscharter/charter/events_manip.rb +175 -0
- data/lib/sscharter/charter/group.rb +130 -0
- data/lib/sscharter/charter/metadata.rb +129 -0
- data/lib/sscharter/charter/story_events.rb +54 -0
- data/lib/sscharter/charter/tip_point.rb +208 -0
- data/lib/sscharter/charter.rb +87 -0
- data/lib/sscharter/tools/svg_path.rb +549 -0
- data/lib/sscharter/tools.rb +6 -0
- data/lib/sscharter/utils.rb +19 -2
- data/lib/sscharter/version.rb +1 -1
- data/lib/sscharter.rb +2 -1074
- data/lock/ruby-3.0.7-bundler-2.5.23-Gemfile.lock +67 -0
- data/lock/ruby-3.1.7-bundler-2.6.9-Gemfile.lock +67 -0
- data/lock/ruby-3.2.11-bundler-4.0.10-Gemfile.lock +98 -0
- data/lock/ruby-3.3.11-bundler-4.0.10-Gemfile.lock +98 -0
- data/lock/ruby-3.4.9-bundler-4.0.10-Gemfile.lock +101 -0
- data/lock/ruby-4.0.2-bundler-4.0.10-Gemfile.lock +101 -0
- data/tutorial/advanced.md +33 -0
- data/tutorial/tools.md +26 -0
- data/tutorial/tutorial.md +6 -32
- metadata +64 -16
- data/Gemfile.lock +0 -48
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @see Sunniesnow::Tools.path
|
|
4
|
+
module Sunniesnow::Tools::SvgPath
|
|
5
|
+
|
|
6
|
+
Vector2D = Sunniesnow::Utils::Data.define :x, :y do
|
|
7
|
+
|
|
8
|
+
# @!attribute [r] x
|
|
9
|
+
# @return [Float]
|
|
10
|
+
# @!attribute [r] y
|
|
11
|
+
# @return [Float]
|
|
12
|
+
|
|
13
|
+
# @return [Vector2D]
|
|
14
|
+
def +@
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Vector2D]
|
|
19
|
+
def -@
|
|
20
|
+
Vector2D.new -x, -y
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param other [Vector2D]
|
|
24
|
+
# @return [Vector2D]
|
|
25
|
+
def + other
|
|
26
|
+
Vector2D.new x + other.x, y + other.y
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param other [Vector2D]
|
|
30
|
+
# @return [Vector2D]
|
|
31
|
+
def - other
|
|
32
|
+
Vector2D.new x - other.x, y - other.y
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param other [Vector2D, Numeric]
|
|
36
|
+
# @return [Vector2D]
|
|
37
|
+
def * other
|
|
38
|
+
other.is_a?(Vector2D) ? Vector2D.new(x*other.x, y*other.y) : Vector2D.new(x*other, y*other)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param other [Vector2D, Numeric]
|
|
42
|
+
# @return [Vector2D]
|
|
43
|
+
def / other
|
|
44
|
+
other.is_a?(Vector2D) ? Vector2D.new(x/other.x, y/other.y) : Vector2D.new(x/other, y/other)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Array<Float>]
|
|
48
|
+
def to_a
|
|
49
|
+
[x, y]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Float]
|
|
53
|
+
def length
|
|
54
|
+
Math.hypot x, y
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Float]
|
|
58
|
+
def angle
|
|
59
|
+
Math.atan2 y, x
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Float]
|
|
63
|
+
def length2
|
|
64
|
+
x*x + y*y
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param angle [Numeric] in radians
|
|
68
|
+
# @return [Vector2D]
|
|
69
|
+
def rotate angle
|
|
70
|
+
cos = Math.cos angle
|
|
71
|
+
sin = Math.sin angle
|
|
72
|
+
Vector2D.new x*cos - y*sin, x*sin + y*cos
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param str [String] in the format "x,y".
|
|
76
|
+
# @return [Vector2D]
|
|
77
|
+
def self.from_string str
|
|
78
|
+
new *str.split(',').map(&:to_f)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param radius [Numeric]
|
|
82
|
+
# @param angle [Numeric] in radians
|
|
83
|
+
# @return [Vector2D]
|
|
84
|
+
def self.from_polar radius, angle
|
|
85
|
+
new Math.cos(angle)*radius, Math.sin(angle)*radius
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class GeometryError < StandardError
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class PathSegment
|
|
93
|
+
|
|
94
|
+
# @return [Vector2D]
|
|
95
|
+
def at t
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param s [Vector2D]
|
|
100
|
+
def at_length s
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Vector2D]
|
|
105
|
+
def begin
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [Vector2D]
|
|
110
|
+
def end
|
|
111
|
+
raise NotImplementedError
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [Float]
|
|
115
|
+
def length
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class Line < PathSegment
|
|
121
|
+
|
|
122
|
+
# @return [Vector2D]
|
|
123
|
+
attr_reader :begin
|
|
124
|
+
|
|
125
|
+
# @return [Vector2D]
|
|
126
|
+
attr_reader :end
|
|
127
|
+
|
|
128
|
+
# @return [Float]
|
|
129
|
+
attr_reader :length
|
|
130
|
+
|
|
131
|
+
# @param begin_point [Vector2D]
|
|
132
|
+
# @param end_point [Vector2D]
|
|
133
|
+
def initialize begin_point, end_point
|
|
134
|
+
@begin = begin_point
|
|
135
|
+
@end = end_point
|
|
136
|
+
@length = (@end - @begin).length
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @param t [Float]
|
|
140
|
+
# @return [Vector2D]
|
|
141
|
+
def at t
|
|
142
|
+
@begin*(1-t) + @end*t
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @param s [Float]
|
|
146
|
+
# @return [Vector2D]
|
|
147
|
+
def at_length s
|
|
148
|
+
at s/@length
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
module Curve
|
|
153
|
+
|
|
154
|
+
# @return [Float]
|
|
155
|
+
def length
|
|
156
|
+
@lengths.last
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @param s [Float]
|
|
160
|
+
# @return [Vector2D]
|
|
161
|
+
def at_length s
|
|
162
|
+
k = (@lengths.bsearch_index { _1 > s } || @lengths.length - 1) - 1
|
|
163
|
+
@segments[k].at_length s - @lengths[k]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param n [Integer]
|
|
167
|
+
# @return [void]
|
|
168
|
+
private def make_segments n
|
|
169
|
+
@segments = Array.new(n) { Line.new at(_1/n.to_f), at((_1+1)/n.to_f) }
|
|
170
|
+
@lengths = @segments.each_with_object([0.0]) { _2.push _2.last + _1.length }
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class QuadraticBezier < PathSegment
|
|
176
|
+
include Curve
|
|
177
|
+
|
|
178
|
+
# @return [Vector2D]
|
|
179
|
+
attr_reader :begin
|
|
180
|
+
|
|
181
|
+
# @return [Vector2D]
|
|
182
|
+
attr_reader :control
|
|
183
|
+
|
|
184
|
+
# @return [Vector2D]
|
|
185
|
+
attr_reader :end
|
|
186
|
+
|
|
187
|
+
# @param begin_point [Vector2D]
|
|
188
|
+
# @param control_point [Vector2D]
|
|
189
|
+
# @param end_point [Vector2D]
|
|
190
|
+
# @param segments [Integer]
|
|
191
|
+
def initialize begin_point, control_point, end_point, segments: 8
|
|
192
|
+
@begin = begin_point
|
|
193
|
+
@control = control_point
|
|
194
|
+
@end = end_point
|
|
195
|
+
make_segments segments
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @param t [Float]
|
|
199
|
+
# @return [Vector2D]
|
|
200
|
+
def at t
|
|
201
|
+
@begin*(1-t)**2 + @control*(2*(1-t)*t) + @end*t**2
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
class CubicBezier < PathSegment
|
|
206
|
+
include Curve
|
|
207
|
+
|
|
208
|
+
# @return [Vector2D]
|
|
209
|
+
attr_reader :begin
|
|
210
|
+
|
|
211
|
+
# @return [Vector2D]
|
|
212
|
+
attr_reader :control1
|
|
213
|
+
|
|
214
|
+
# @return [Vector2D]
|
|
215
|
+
attr_reader :control2
|
|
216
|
+
|
|
217
|
+
# @return [Vector2D]
|
|
218
|
+
attr_reader :end
|
|
219
|
+
|
|
220
|
+
# @param begin_point [Vector2D]
|
|
221
|
+
# @param control_point1 [Vector2D]
|
|
222
|
+
# @param control_point2 [Vector2D]
|
|
223
|
+
# @param end_point [Vector2D]
|
|
224
|
+
# @param segments [Integer]
|
|
225
|
+
def initialize begin_point, control_point1, control_point2, end_point, segments: 16
|
|
226
|
+
@begin = begin_point
|
|
227
|
+
@control1 = control_point1
|
|
228
|
+
@control2 = control_point2
|
|
229
|
+
@end = end_point
|
|
230
|
+
make_segments segments
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# @param t [Float]
|
|
234
|
+
# @return [Vector2D]
|
|
235
|
+
def at t
|
|
236
|
+
@begin*(1-t)**3 + @control1*(3*(1-t)**2*t) + @control2*(3*(1-t)*t**2) + @end*t**3
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class Arc < PathSegment
|
|
241
|
+
include Curve
|
|
242
|
+
|
|
243
|
+
# @return [Vector2D]
|
|
244
|
+
attr_reader :begin
|
|
245
|
+
|
|
246
|
+
# @return [Numeric]
|
|
247
|
+
attr_reader :radius
|
|
248
|
+
|
|
249
|
+
# @return [Numeric]
|
|
250
|
+
attr_reader :rotation
|
|
251
|
+
|
|
252
|
+
# @return [Boolean]
|
|
253
|
+
attr_reader :large_arc
|
|
254
|
+
|
|
255
|
+
# @return [Boolean]
|
|
256
|
+
attr_reader :sweep_positive
|
|
257
|
+
|
|
258
|
+
# @return [Vector2D]
|
|
259
|
+
attr_reader :end
|
|
260
|
+
|
|
261
|
+
# @param begin_point [Vector2D]
|
|
262
|
+
# @param radius [Vector2D]
|
|
263
|
+
# @param rotation [Numeric]
|
|
264
|
+
# @param large_arc [Boolean]
|
|
265
|
+
# @param sweep_positive [Boolean]
|
|
266
|
+
# @param end_point [Vector2D]
|
|
267
|
+
# @param segments [Integer]
|
|
268
|
+
def initialize begin_point, radius, rotation, large_arc, sweep_positive, end_point, segments: 8
|
|
269
|
+
@begin = begin_point
|
|
270
|
+
@radius = radius
|
|
271
|
+
@rotation = rotation
|
|
272
|
+
@large_arc = large_arc
|
|
273
|
+
@sweep_positive = sweep_positive
|
|
274
|
+
@end = end_point
|
|
275
|
+
raise GeometryError.new 'Radius must be positive' if @radius.x <= 0 || @radius.y <= 0
|
|
276
|
+
raise GeometryError.new 'Cannot draw arc with same begin and end point' if @begin == @end
|
|
277
|
+
solve_center
|
|
278
|
+
make_segments segments
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# @param t [Float]
|
|
282
|
+
# @return [Vector2D]
|
|
283
|
+
def at t
|
|
284
|
+
@center + (Vector2D.from_polar(1, @angle1*(1-t) + @angle2*t) * @radius).rotate(@rotation)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# @return [Vector2D]
|
|
288
|
+
private def solve_center
|
|
289
|
+
p1 = @begin.rotate(-@rotation) / @radius
|
|
290
|
+
p2 = @end.rotate(-@rotation) / @radius
|
|
291
|
+
diff = p2 - p1
|
|
292
|
+
if (distance2 = diff.length2) > 4
|
|
293
|
+
scale = Math.sqrt(distance2)/2
|
|
294
|
+
distance2 = 4
|
|
295
|
+
@radius *= scale
|
|
296
|
+
diff /= scale
|
|
297
|
+
end
|
|
298
|
+
normal = Vector2D.new -diff.y, diff.x
|
|
299
|
+
sign = @large_arc == @sweep_positive ? -1 : 1
|
|
300
|
+
center = (p1 + p2)/2 + normal * (sign * Math.sqrt(1.0/distance2 - 0.25))
|
|
301
|
+
@angle1 = (p1 - center).angle
|
|
302
|
+
@angle2 = (p2 - center).angle
|
|
303
|
+
if @angle2 > @angle1 != @sweep_positive
|
|
304
|
+
@angle2 += (@sweep_positive ? 1 : -1) * 2*Math::PI
|
|
305
|
+
end
|
|
306
|
+
@center = (center * @radius).rotate @rotation
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
class CommandError < StandardError
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
class Command
|
|
314
|
+
|
|
315
|
+
# @param name [Symbol]
|
|
316
|
+
# @param parameters [Array<Symbol>]
|
|
317
|
+
# @param follow_up [Symbol]
|
|
318
|
+
def initialize name, parameters, follow_up = name
|
|
319
|
+
@name = name
|
|
320
|
+
@parameters = parameters
|
|
321
|
+
@follow_up = follow_up
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# @param path [Path]
|
|
325
|
+
# @param tokens [Array<String>]
|
|
326
|
+
# @return [void]
|
|
327
|
+
def parse path, tokens
|
|
328
|
+
relative = (?a..?z).include? tokens.shift
|
|
329
|
+
name = @name.to_sym
|
|
330
|
+
begin
|
|
331
|
+
arguments = @parameters.map do |parameter|
|
|
332
|
+
case parameter
|
|
333
|
+
when :point
|
|
334
|
+
point = Vector2D.new tokens.shift.to_f, tokens.shift.to_f
|
|
335
|
+
point += path.current if relative
|
|
336
|
+
point
|
|
337
|
+
when :point_x
|
|
338
|
+
x = tokens.shift.to_f
|
|
339
|
+
Vector2D.new relative ? path.current.x + x : x, path.current.y
|
|
340
|
+
when :point_y
|
|
341
|
+
y = tokens.shift.to_f
|
|
342
|
+
Vector2D.new path.current.x, relative ? path.current.y + y : y
|
|
343
|
+
when :vector
|
|
344
|
+
Vector2D.new tokens.shift.to_f, tokens.shift.to_f
|
|
345
|
+
when :number
|
|
346
|
+
tokens.shift.to_f
|
|
347
|
+
when :boolean
|
|
348
|
+
raise "Invalid boolean: #{tokens.first}" unless %w[0 1].include? tokens.first
|
|
349
|
+
tokens.shift == ?1
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
path.public_send name, *arguments
|
|
353
|
+
name = @follow_up.to_sym
|
|
354
|
+
end until tokens.empty? || tokens.first =~ /[a-zA-Z]/
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
class Path
|
|
359
|
+
COMMANDS = {
|
|
360
|
+
m: Command.new(:move_to, %i[point], :line_to),
|
|
361
|
+
l: Command.new(:line_to, %i[point]),
|
|
362
|
+
h: Command.new(:line_to, %i[point_x]),
|
|
363
|
+
v: Command.new(:line_to, %i[point_y]),
|
|
364
|
+
z: Command.new(:close, %i[], :unexpected_token),
|
|
365
|
+
c: Command.new(:cubic, %i[point point point]),
|
|
366
|
+
s: Command.new(:smooth_cubic, %i[point point]),
|
|
367
|
+
q: Command.new(:quadratic, %i[point point]),
|
|
368
|
+
t: Command.new(:smooth_quadratic, %i[point]),
|
|
369
|
+
a: Command.new(:arc, %i[vector number boolean boolean point]),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# @return [Vector2D]
|
|
373
|
+
attr_reader :current
|
|
374
|
+
|
|
375
|
+
# @param data [String]
|
|
376
|
+
def initialize data
|
|
377
|
+
@segments = []
|
|
378
|
+
@lengths = [0.0]
|
|
379
|
+
@last_initial = 0
|
|
380
|
+
@current = Vector2D.new 0.0, 0.0
|
|
381
|
+
data = data.gsub /[a-df-zA-DF-Z]/, ' \0 ' # exclude E for scientific notation
|
|
382
|
+
data.gsub! /[,\s]+/, ' '
|
|
383
|
+
data.strip!
|
|
384
|
+
parse_instructions data.split
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# @raise [CommandError]
|
|
388
|
+
# @return [void]
|
|
389
|
+
def unexpected_token
|
|
390
|
+
raise CommandError.new 'Unexpected token'
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# @param point [Vector2D]
|
|
394
|
+
# @return [Vector2D]
|
|
395
|
+
def move_to point
|
|
396
|
+
@last_initial = @segments.length
|
|
397
|
+
@current = point
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# @param point [Vector2D]
|
|
401
|
+
# @return [Vector2D]
|
|
402
|
+
def line_to point
|
|
403
|
+
add_segment Line.new @current, point
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# @return [Vector2D]
|
|
407
|
+
def close
|
|
408
|
+
point = @segments[@last_initial].begin
|
|
409
|
+
@current == point ? point : line_to(point)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# @param control1 [Vector2D]
|
|
413
|
+
# @param control2 [Vector2D]
|
|
414
|
+
# @param point [Vector2D]
|
|
415
|
+
# @return [Vector2D]
|
|
416
|
+
def cubic control1, control2, point
|
|
417
|
+
add_segment CubicBezier.new @current, control1, control2, point
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# @param control2 [Vector2D]
|
|
421
|
+
# @param point [Vector2D]
|
|
422
|
+
# @return [Vector2D]
|
|
423
|
+
def smooth_cubic control2, point
|
|
424
|
+
last = @segments.last
|
|
425
|
+
control1 = last.is_a?(CubicBezier) ? @current*2 - last.control2 : @current
|
|
426
|
+
cubic control1, control2, point
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# @param control [Vector2D]
|
|
430
|
+
# @param point [Vector2D]
|
|
431
|
+
# @return [Vector2D]
|
|
432
|
+
def quadratic control, point
|
|
433
|
+
add_segment QuadraticBezier.new @current, control, point
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# @param point [Vector2D]
|
|
437
|
+
# @return [Vector2D]
|
|
438
|
+
def smooth_quadratic point
|
|
439
|
+
last = @segments.last
|
|
440
|
+
control = last.is_a?(QuadraticBezier) ? @current*2 - last.control : @current
|
|
441
|
+
quadratic control, point
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# @param radius [Numeric]
|
|
445
|
+
# @param rotation [Numeric]
|
|
446
|
+
# @param large_arc [Boolean]
|
|
447
|
+
# @param sweep_positive [Boolean]
|
|
448
|
+
# @param point [Vector2D]
|
|
449
|
+
# @return [Vector2D]
|
|
450
|
+
def arc radius, rotation, large_arc, sweep_positive, point
|
|
451
|
+
add_segment Arc.new @current, radius, rotation*Math::PI/180, large_arc, sweep_positive, point
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# @param segment [PathSegment]
|
|
455
|
+
# @return [Vector2D]
|
|
456
|
+
def add_segment segment
|
|
457
|
+
@segments.push segment
|
|
458
|
+
@lengths.push @lengths.last + segment.length
|
|
459
|
+
@current = segment.end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# @return [Float]
|
|
463
|
+
def length
|
|
464
|
+
@lengths.last
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# @param s [Float]
|
|
468
|
+
# @return [Vector2D]
|
|
469
|
+
def at_length s
|
|
470
|
+
len = length
|
|
471
|
+
s %= len*2
|
|
472
|
+
s = 2*len - s if s > len
|
|
473
|
+
i = @lengths.bsearch_index { _1 > s } - 1
|
|
474
|
+
@segments[i].at_length s - @lengths[i]
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# @param range [Range, Integer]
|
|
478
|
+
# @param total [Integer]
|
|
479
|
+
# @yieldparam x [Float]
|
|
480
|
+
# @yieldparam y [Float]
|
|
481
|
+
# @yieldparam i [Integer]
|
|
482
|
+
# @return [Path] +self+.
|
|
483
|
+
def samples range, total = range.is_a?(Range) ? range.end : range
|
|
484
|
+
return to_enum __method__, range, total unless block_given?
|
|
485
|
+
range = 0...range unless range.is_a? Range
|
|
486
|
+
len = length
|
|
487
|
+
delta = len / total.to_f
|
|
488
|
+
s = range.begin * delta
|
|
489
|
+
sign = 1
|
|
490
|
+
s %= len*2
|
|
491
|
+
if s > len
|
|
492
|
+
s = 2*len - s
|
|
493
|
+
sign *= -1
|
|
494
|
+
end
|
|
495
|
+
i = @lengths.bsearch_index { _1 > s } - 1
|
|
496
|
+
range.each do |j|
|
|
497
|
+
if j == range.begin
|
|
498
|
+
yield *@segments[i].at_length(s - @lengths[i]).to_a, j
|
|
499
|
+
next
|
|
500
|
+
end
|
|
501
|
+
s += delta*sign
|
|
502
|
+
s %= len*2
|
|
503
|
+
if s > len
|
|
504
|
+
s = 2*len - s
|
|
505
|
+
sign *= -1
|
|
506
|
+
end
|
|
507
|
+
i -= 1 while i > 0 && s < @lengths[i]
|
|
508
|
+
i += 1 while i < @lengths.length - 1 && s > @lengths[i+1]
|
|
509
|
+
yield *@segments[i].at_length(s - @lengths[i]).to_a, j
|
|
510
|
+
end
|
|
511
|
+
self
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# @param tokens [Array<String>]
|
|
515
|
+
# @return [void]
|
|
516
|
+
private def parse_instructions tokens
|
|
517
|
+
while instruction = tokens.first
|
|
518
|
+
command = COMMANDS[instruction.downcase.to_sym]
|
|
519
|
+
raise CommandError.new "Unknown command #{instruction}" unless command
|
|
520
|
+
command.parse self, tokens
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
module Sunniesnow::Tools
|
|
527
|
+
|
|
528
|
+
# Turn any SVG path into uniformly sampled points.
|
|
529
|
+
# @param data [String] The SVG path definition.
|
|
530
|
+
# @return [SvgPath::Path]
|
|
531
|
+
# @yieldparam x [Float]
|
|
532
|
+
# @yieldparam y [Float]
|
|
533
|
+
# @yieldparam i [Integer]
|
|
534
|
+
# @overload path data, range, total = range.end, &block
|
|
535
|
+
# @param range [Range]
|
|
536
|
+
# @param total [Integer]
|
|
537
|
+
# @overload path data, range_end, total = range_end, &block
|
|
538
|
+
# @param range_end [Integer]
|
|
539
|
+
# @param total [Integer]
|
|
540
|
+
# @example
|
|
541
|
+
# path 'M -6,-0 C -4,2 -0,-0.7 -0,2 -0,4.7 6,-1 5,1 4,3 1,2 -0,-0 -1,-2 10,-2 6,-3 2,-4 -6,-0 -4,2 c 2,2 3,-8 0,-5', 128 do |x, y, i|
|
|
542
|
+
# d x, y; b 1/32r
|
|
543
|
+
# end
|
|
544
|
+
module_function def path data, *args, &block
|
|
545
|
+
result = SvgPath::Path.new data
|
|
546
|
+
result.samples *args, &block if block
|
|
547
|
+
result
|
|
548
|
+
end
|
|
549
|
+
end
|
data/lib/sscharter/utils.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @note Internal API.
|
|
1
4
|
module Sunniesnow::Utils
|
|
2
5
|
|
|
3
6
|
refine Array do
|
|
4
|
-
# If there
|
|
5
|
-
#
|
|
7
|
+
# Do a binary search. If there is a match, return the index of the match.
|
|
8
|
+
# Otherwise, if +right+, return the index of the last element that is less than the value;
|
|
9
|
+
# otherwise, return the index of the first element that is greater than the value.
|
|
6
10
|
def bisect value = nil, right: false, &compare_function
|
|
7
11
|
if value && compare_function
|
|
8
12
|
raise ArgumentError, "Cannot specify both value and compare_function"
|
|
@@ -38,4 +42,17 @@ module Sunniesnow::Utils
|
|
|
38
42
|
to_s.snake_to_camel.to_sym
|
|
39
43
|
end
|
|
40
44
|
end
|
|
45
|
+
|
|
46
|
+
if Object.const_defined? :Data # Ruby 3.2
|
|
47
|
+
Data = Data
|
|
48
|
+
else
|
|
49
|
+
class Data < Struct
|
|
50
|
+
def self.define *fields, &block
|
|
51
|
+
new *fields do
|
|
52
|
+
fields.each { undef_method "#{_1}=" }
|
|
53
|
+
class_eval &block if block
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
41
58
|
end
|