dyi 0.0.2 → 1.0.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,902 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ #
7
+ # This file is part of DYI.
8
+ #
9
+ # DYI is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # DYI is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ require 'enumerator'
23
+
24
+ module DYI #:nodoc:
25
+ module Shape #:nodoc:
26
+
27
+ class Path < Base
28
+
29
+ def initialize(start_point, options={})
30
+ @path_data = case start_point
31
+ when PathData then start_point
32
+ else PathData.new(start_point)
33
+ end
34
+ @attributes = init_attributes(options)
35
+ end
36
+
37
+ def move_to(*points)
38
+ push_command(:move_to, *points)
39
+ end
40
+
41
+ def rmove_to(*points)
42
+ push_command(:rmove_to, *points)
43
+ end
44
+
45
+ def line_to(*points)
46
+ push_command(:line_to, *points)
47
+ end
48
+
49
+ def rline_to(*points)
50
+ push_command(:rline_to, *points)
51
+ end
52
+
53
+ def quadratic_curve_to(*points)
54
+ raise ArgumentError, "number of points must be 2 or more" if points.size < 2
55
+ push_command(:quadratic_curve_to, points[0], points[1])
56
+ push_command(:shorthand_quadratic_curve_to, *points[2..-1]) if points.size > 2
57
+ end
58
+
59
+ def rquadratic_curve_to(*points)
60
+ raise ArgumentError, "number of points must be 2 or more" if points.size < 2
61
+ push_command(:rquadratic_curve_to, points[0], points[1])
62
+ push_command(:rshorthand_quadratic_curve_to, *points[2..-1]) if points.size > 2
63
+ end
64
+
65
+ def curve_to(*points)
66
+ raise ArgumentError, "number of points must be odd number of 3 or more" if points.size % 2 == 0 || points.size < 3
67
+ push_command(:curve_to, points[0], points[1], points[2])
68
+ push_command(:shorthand_curve_to, *points[3..-1]) if points.size > 3
69
+ end
70
+
71
+ def rcurve_to(*points)
72
+ raise ArgumentError, "number of points must be odd number of 3 or more" if points.size % 2 == 0 || points.size < 3
73
+ push_command(:rcurve_to, points[0], points[1], points[2])
74
+ push_command(:rshorthand_curve_to, *points[3..-1]) if points.size > 3
75
+ end
76
+
77
+ def arc_to(point, radius_x, radius_y, rotation=0, is_large_arc=false, is_clockwise=true)
78
+ push_command(:arc_to, radius_x, radius_y, rotation, is_large_arc, is_clockwise, point)
79
+ end
80
+
81
+ def rarc_to(point, radius_x, radius_y, rotation=0, is_large_arc=false, is_clockwise=true)
82
+ push_command(:rarc_to, radius_x, radius_y, rotation, is_large_arc, is_clockwise, point)
83
+ end
84
+
85
+ def close?
86
+ @path_data.close?
87
+ end
88
+
89
+ def close_path
90
+ push_command(:close_path)
91
+ end
92
+
93
+ def start_point
94
+ @path_data.start_point
95
+ end
96
+
97
+ def current_point
98
+ @path_data.current_point
99
+ end
100
+
101
+ def current_start_point
102
+ @path_data.current_start_point
103
+ end
104
+
105
+ def push_command(command_type, *args)
106
+ @path_data.push_command(command_type, *args)
107
+ end
108
+
109
+ def pop_command
110
+ @path_data.pop
111
+ end
112
+
113
+ def path_points
114
+ @path_data.path_points
115
+ end
116
+
117
+ def path_data
118
+ @path_data
119
+ end
120
+
121
+ def compatible_path_data
122
+ @path_data.compatible_path_data
123
+ end
124
+
125
+ def concise_path_data
126
+ @path_data.to_concise_syntax
127
+ end
128
+
129
+ def left
130
+ edge_coordinate(:left)
131
+ end
132
+
133
+ def right
134
+ edge_coordinate(:right)
135
+ end
136
+
137
+ def top
138
+ edge_coordinate(:top)
139
+ end
140
+
141
+ def bottom
142
+ edge_coordinate(:bottom)
143
+ end
144
+ =begin
145
+ def line_bezier_paths
146
+ start_point = Coordinate::ZERO
147
+ current_point = Coordinate::ZERO
148
+ last_ctrl_point = nil
149
+ @path_data.inject([]) do |result, path_point|
150
+ case path_point.first
151
+ when 'M', 'L', 'C'
152
+ last_ctrl_point = path_point[2]
153
+ current_point = path_point.last
154
+ result << path_point
155
+ start_point = current_point if path_point.first == 'M'
156
+ when 'm', 'l'
157
+ result << [path_point.first.upcase, (current_point += path_point.last)]
158
+ start_point = current_point if path_point.first == 'm'
159
+ when 'c'
160
+ result << [path_point.first.upcase, current_point + path_point[1], (last_ctrl_point = current_point + path_point[2]), (current_point += path_point.last)]
161
+ when 'Z'
162
+ result << path_point
163
+ current_point = start_point
164
+ when 'Q', 'q', 'T', 't'
165
+ case path_point.first
166
+ when 'Q'
167
+ last_ctrl_point = path_point[1]
168
+ last_point = path_point[2]
169
+ when 'q'
170
+ last_ctrl_point = current_point + path_point[1]
171
+ last_point = current_point + path_point[2]
172
+ when 'T'
173
+ last_ctrl_point = current_point * 2 - last_ctrl_point
174
+ last_point = path_point[1]
175
+ when 't'
176
+ last_ctrl_point = current_point * 2 - last_ctrl_point
177
+ last_point = current_point + path_point[1]
178
+ end
179
+ ctrl_point1 = (current_point + last_ctrl_point * 2).quo(3)
180
+ ctrl_point2 = (last_point + last_ctrl_point * 2).quo(3)
181
+ result << ['C', ctrl_point1, ctrl_point2, (current_point = last_point)]
182
+ when 'S', 's'
183
+ case path_point.first
184
+ when 'S'
185
+ ctrl_point1 = current_point * 2 - last_ctrl_point
186
+ ctrl_point2 = path_point[1]
187
+ last_point = path_point[2]
188
+ when 's'
189
+ ctrl_point1 = current_point * 2 - last_ctrl_point
190
+ ctrl_point2 = current_point + path_point[1]
191
+ last_point = current_point + path_point[2]
192
+ end
193
+ result << ['C', ctrl_point1, (last_ctrl_point = ctrl_point2), (current_point = last_point)]
194
+ when 'A', 'a'
195
+ rx, ry, lotate, large_arc, clockwise, last_point = path_point[1..-1]
196
+ last_point += current_point if path_point.first == 'a'
197
+ rx = rx.to_f
198
+ ry = ry.to_f
199
+ lotate = lotate * Math::PI / 180
200
+ cu_pt = Coordinate.new(
201
+ current_point.x * Math.cos(lotate) / rx + current_point.y * Math.sin(lotate) / rx,
202
+ current_point.y * Math.cos(lotate) / ry - current_point.x * Math.sin(lotate) / ry)
203
+ en_pt = Coordinate.new(
204
+ last_point.x * Math.cos(lotate) / rx + last_point.y * Math.sin(lotate) / rx,
205
+ last_point.y * Math.cos(lotate) / ry - last_point.x * Math.sin(lotate) / ry)
206
+ begin
207
+ k = Math.sqrt(4.quo((en_pt.x.to_f - cu_pt.x.to_f) ** 2 + (en_pt.y.to_f - cu_pt.y.to_f) ** 2) - 1) * (large_arc == clockwise ? 1 : -1)
208
+ center_pt = Coordinate.new(
209
+ cu_pt.x - cu_pt.y * k + en_pt.x + en_pt.y * k,
210
+ cu_pt.y + cu_pt.x * k + en_pt.y - en_pt.x * k) * 0.5
211
+ cu_pt -= center_pt
212
+ en_pt -= center_pt
213
+ theta = Math.acos(cu_pt.x.to_f * en_pt.x.to_f + cu_pt.y.to_f * en_pt.y.to_f)
214
+ theta = 2 * Math::PI - theta if large_arc == 1
215
+ rescue
216
+ center_pt = Coordinate.new(cu_pt.x + en_pt.x, cu_pt.y + en_pt.y) * 0.5
217
+ cu_pt -= center_pt
218
+ en_pt -= center_pt
219
+ theta = Math::PI
220
+ end
221
+ d_count = theta.quo(Math::PI / 8).ceil
222
+ d_t = theta / d_count * (clockwise == 1 ? 1 : -1)
223
+ curves = []
224
+ cos = Math.cos(d_t)
225
+ sin = Math.sin(d_t)
226
+ tan = Math.tan(d_t / 4)
227
+ mat = Matrix.new(
228
+ rx * Math.cos(lotate), rx * Math.sin(lotate),
229
+ -ry * Math.sin(lotate), ry * Math.cos(lotate),
230
+ center_pt.x * rx * Math.cos(lotate) - center_pt.y * ry * Math.sin(lotate),
231
+ center_pt.y * ry * Math.cos(lotate) + center_pt.x * rx * Math.sin(lotate))
232
+ d_count.times do |i|
233
+ ne_pt = Coordinate.new(cu_pt.x * cos - cu_pt.y * sin, cu_pt.y * cos + cu_pt.x * sin)
234
+ curves << [
235
+ mat.translate(Coordinate.new(cu_pt.x - cu_pt.y * 4 * tan / 3, cu_pt.y + cu_pt.x * 4 * tan / 3)),
236
+ mat.translate(Coordinate.new(ne_pt.x + ne_pt.y * 4 * tan / 3, ne_pt.y - ne_pt.x * 4 * tan / 3)),
237
+ mat.translate(ne_pt)]
238
+ cu_pt = ne_pt
239
+ end
240
+ curves.last[2] = last_point
241
+ current_point = last_point
242
+ curves.each do |c|
243
+ result << ['C', c[0], c[1], c[2]]
244
+ end
245
+ end
246
+ result
247
+ end
248
+ end
249
+ =end
250
+ def write_as(formatter, io=$>)
251
+ formatter.write_path(self, io, &(block_given? ? Proc.new : nil))
252
+ end
253
+
254
+ private
255
+ =begin
256
+ def edge_coordinate(edge_type)
257
+ case edge_type
258
+ when :left
259
+ element_type = :x
260
+ amount_type = :min
261
+ when :right
262
+ element_type = :x
263
+ amount_type = :max
264
+ when :top
265
+ element_type = :y
266
+ amount_type = :min
267
+ when :bottom
268
+ element_type = :y
269
+ amount_type = :max
270
+ else
271
+ raise ArgumentError, "unknown edge_tpe `#{edge_type}'"
272
+ end
273
+ current_pt = nil
274
+ line_bezier_paths.inject(nil) do |result, path_point|
275
+ case path_point.first
276
+ when 'M', 'L'
277
+ current_pt = path_point.last
278
+ [result, current_pt.__send__(element_type)].compact.__send__(amount_type)
279
+ when 'C'
280
+ pts = [current_pt.__send__(element_type), path_point[1].__send__(element_type), path_point[2].__send__(element_type), path_point[3].__send__(element_type)]
281
+ nums = pts.map {|pt| pt.to_f}
282
+ current_pt = path_point.last
283
+ delta = (nums[2] - nums[1] * 2 + nums[0]) ** 2 - (nums[3] - nums[2] * 3 + nums[1] * 3 - nums[0]) * (nums[1] - nums[0])
284
+ if delta >= 0
285
+ res0 = ((nums[2] - nums[1] * 2 + nums[0]) * (-1) + Math.sqrt(delta)).quo(nums[3] - nums[2] * 3 + nums[1] * 3 - nums[0])
286
+ res1 = ((nums[2] - nums[1] * 2 + nums[0]) * (-1) - Math.sqrt(delta)).quo(nums[3] - nums[2] * 3 + nums[1] * 3 - nums[0])
287
+ res0 = (0..1).include?(res0) ? Length.new(res0) : nil
288
+ res1 = (0..1).include?(res1) ? Length.new(res1) : nil
289
+ [result, pts[0], pts[3], res0, res1].compact.__send__(amount_type)
290
+ else
291
+ [result, pts[0], pts[3]].conpact.__send__(amount_type)
292
+ end
293
+ else
294
+ result
295
+ end
296
+ end
297
+ end
298
+ =end
299
+ class << self
300
+
301
+ public
302
+
303
+ def draw(start_point, options={}, &block)
304
+ path = new(start_point, options)
305
+ yield path
306
+ path
307
+ end
308
+
309
+ def draw_and_close(start_point, options={}, &block)
310
+ path = draw(start_point, options, &block)
311
+ path.close_path unless path.close?
312
+ path
313
+ end
314
+ end
315
+
316
+ class PathData #:nodoc:
317
+ include Enumerable
318
+
319
+ def initialize(*points)
320
+ raise ArgumentError, 'wrong number of arguments (0 for 1)' if points.empty?
321
+ @commands = MoveCommand.absolute_commands(nil, *points)
322
+ end
323
+
324
+ def each
325
+ if block_given?
326
+ @commands.each{|command| yield command}
327
+ else
328
+ @commands.each
329
+ end
330
+ end
331
+
332
+ def push_command(command_type, *args)
333
+ case command_type
334
+ when :move_to
335
+ @commands.push(*MoveCommand.absolute_commands(@commands.last, *args))
336
+ when :rmove_to
337
+ @commands.push(*MoveCommand.relative_commands(@commands.last, *args))
338
+ when :close_path
339
+ @commands.push(*CloseCommand.commands(@commands.last))
340
+ when :line_to
341
+ @commands.push(*LineCommand.absolute_commands(@commands.last, *args))
342
+ when :rline_to
343
+ @commands.push(*LineCommand.relative_commands(@commands.last, *args))
344
+ when :horizontal_lineto_to
345
+ @commands.push(*HorizontalLineCommand.absolute_commands(@commands.last, *args))
346
+ when :rhorizontal_lineto_to
347
+ @commands.push(*HorizontalLineCommand.relative_commands(@commands.last, *args))
348
+ when :vertical_lineto_to
349
+ @commands.push(*VerticalLineCommand.absolute_commands(@commands.last, *args))
350
+ when :rvertical_lineto_to
351
+ @commands.push(*VerticalLineCommand.relative_commands(@commands.last, *args))
352
+ when :curve_to
353
+ @commands.push(*CurveCommand.absolute_commands(@commands.last, *args))
354
+ when :rcurve_to
355
+ @commands.push(*CurveCommand.relative_commands(@commands.last, *args))
356
+ when :shorthand_curve_to
357
+ @commands.push(*ShorthandCurveCommand.absolute_commands(@commands.last, *args))
358
+ when :rshorthand_curve_to
359
+ @commands.push(*ShorthandCurveCommand.relative_commands(@commands.last, *args))
360
+ when :quadratic_curve_to
361
+ @commands.push(*QuadraticCurveCommand.absolute_commands(@commands.last, *args))
362
+ when :rquadratic_curve_to
363
+ @commands.push(*QuadraticCurveCommand.relative_commands(@commands.last, *args))
364
+ when :shorthand_quadratic_curve_to
365
+ @commands.push(*ShorthandQuadraticCurveCommand.absolute_commands(@commands.last, *args))
366
+ when :rshorthand_quadratic_curve_to
367
+ @commands.push(*ShorthandQuadraticCurveCommand.relative_commands(@commands.last, *args))
368
+ when :arc_to
369
+ @commands.push(*ArcCommand.absolute_commands(@commands.last, *args))
370
+ when :rarc_to
371
+ @commands.push(*ArcCommand.relative_commands(@commands.last, *args))
372
+ else
373
+ raise ArgumentError, "unknown command type `#{command_type}'"
374
+ end
375
+ end
376
+
377
+ def pop_command
378
+ @commands.pop
379
+ end
380
+
381
+ def compatible_path_data
382
+ new_instance = clone
383
+ new_instance.commands = compatible_path_commands
384
+ new_instance
385
+ end
386
+
387
+ def compatible_path_data!
388
+ @commands = compatible_path_commands
389
+ self
390
+ end
391
+
392
+ def start_point
393
+ @commands.first.start_point
394
+ end
395
+
396
+ def current_point
397
+ @commands.last.last_point
398
+ end
399
+
400
+ def current_start_point
401
+ @commands.last.start_point
402
+ end
403
+
404
+ def path_points
405
+ @commands.map{|command| command.points}.flatten
406
+ end
407
+
408
+ def close?
409
+ @commands.last.is_a?(CloseCommand)
410
+ end
411
+
412
+ def to_concise_syntax
413
+ @commands.map{|command| command.to_concise_syntax_fragments}.join(' ')
414
+ end
415
+
416
+ protected
417
+
418
+ def commands=(value)
419
+ @commands = value
420
+ end
421
+
422
+ private
423
+
424
+ def compatible_path_commands
425
+ @commands.inject([]) do |compat_cmds, command|
426
+ compat_cmds.push(*command.to_compatible_commands(compat_cmds.last))
427
+ end
428
+ end
429
+ end
430
+
431
+ class CommandBase #:nodoc:
432
+ attr_reader :preceding_command, :point
433
+
434
+ def initialize(relative, preceding_command, point)
435
+ @relative = relative
436
+ @preceding_command = preceding_command
437
+ @point = Coordinate.new(point)
438
+ end
439
+
440
+ def relative?
441
+ @relative
442
+ end
443
+
444
+ def absolute?
445
+ !relative?
446
+ end
447
+
448
+ def start_point
449
+ preceding_command.start_point
450
+ end
451
+
452
+ def last_point
453
+ relative? ? preceding_point + @point : @point
454
+ end
455
+
456
+ def preceding_point
457
+ preceding_command && preceding_command.last_point
458
+ end
459
+
460
+ def to_compatible_commands(preceding_command)
461
+ compat_commands = clone
462
+ compat_commands.preceding_command = preceding_command
463
+ compat_commands
464
+ end
465
+
466
+ def used_same_command?
467
+ preceding_command.instructions_char == instructions_char
468
+ end
469
+
470
+ protected
471
+
472
+ def preceding_command=(value)
473
+ @preceding_command = preceding_command
474
+ end
475
+
476
+ class << self
477
+ def relative_commands(preceding_command, *args)
478
+ commands(true, preceding_command, *args)
479
+ end
480
+
481
+ def absolute_commands(preceding_command, *args)
482
+ commands(false, preceding_command, *args)
483
+ end
484
+ end
485
+ end
486
+
487
+ class MoveCommand < CommandBase #:nodoc:
488
+
489
+ def start_point
490
+ last_point
491
+ end
492
+
493
+ def last_point
494
+ (relative? && preceding_command.nil?) ? point : super
495
+ end
496
+
497
+ def relative?
498
+ preceding_command.nil? ? false : super
499
+ end
500
+
501
+ def to_concise_syntax_fragments
502
+ instructions_char + @point.to_s
503
+ end
504
+
505
+ def instructions_char
506
+ relative? ? 'm' : 'M'
507
+ end
508
+
509
+ class << self
510
+ def commands(relative, preceding_command, *points)
511
+ raise ArgumentError, 'wrong number of arguments (2 for 3)' if points.empty?
512
+ commands = [new(relative, preceding_command, points.first)]
513
+ points[1..-1].inject(commands) do |cmds, pt|
514
+ cmds << LineCommand.new(relative, cmds.last, pt)
515
+ end
516
+ end
517
+ end
518
+ end
519
+
520
+ class CloseCommand < CommandBase #:nodoc:
521
+ def initialize(preceding_command)
522
+ raise ArgumentError, 'preceding_command is nil' if preceding_command.nil?
523
+ @relative = nil
524
+ @preceding_command = preceding_command
525
+ @point = nil
526
+ end
527
+
528
+ def last_point
529
+ start_point
530
+ end
531
+
532
+ def relative?
533
+ nil
534
+ end
535
+
536
+ def absolute?
537
+ nil
538
+ end
539
+
540
+ def to_concise_syntax_fragments
541
+ instructions_char
542
+ end
543
+
544
+ def instructions_char
545
+ 'Z'
546
+ end
547
+
548
+ class << self
549
+ undef relative_commands, absolute_commands
550
+
551
+ def commands(preceding_command)
552
+ [new(preceding_command)]
553
+ end
554
+ end
555
+ end
556
+
557
+ class LineCommand < CommandBase #:nodoc:
558
+ def initialize(relative, preceding_command, point)
559
+ raise ArgumentError, 'preceding_command is nil' if preceding_command.nil?
560
+ super
561
+ end
562
+
563
+ def to_concise_syntax_fragments
564
+ used_same_command? ? @point.to_s : (instructions_char + @point.to_s)
565
+ end
566
+
567
+ def instructions_char
568
+ relative? ? 'l' : 'L'
569
+ end
570
+
571
+ class << self
572
+ def commands(relative, preceding_command, *points)
573
+ raise ArgumentError, 'wrong number of arguments (2 for 3)' if points.empty?
574
+ cmd = preceding_command
575
+ points.inject([]) do |cmds, pt|
576
+ cmds << (cmd = new(relative, cmd, pt))
577
+ end
578
+ end
579
+ end
580
+ end
581
+
582
+ class HorizontalLineCommand < LineCommand #:nodoc:
583
+ def initialize(relative, preceding_command, x)
584
+ super(relative, preceding_command, Coordinate.new(x, relative ? 0 : preceding_command.last_point.y))
585
+ end
586
+
587
+ def to_compatible_commands(preceding_command)
588
+ LineCommand.new(relative?, preceding_command, @point)
589
+ end
590
+
591
+ def to_concise_syntax_fragments
592
+ used_same_command? ? @point.x.to_s : (instructions_char + @point.x.to_s)
593
+ end
594
+
595
+ def instructions_char
596
+ relative? ? 'h' : 'H'
597
+ end
598
+ end
599
+
600
+ class VerticalLineCommand < LineCommand #:nodoc:
601
+ def initialize(relative, preceding_command, y)
602
+ super(relative, preceding_command, Coordinate.new(relative ? 0 : preceding_command.last_point.x, y))
603
+ end
604
+
605
+ def to_compatible_commands(preceding_command)
606
+ LineCommand.new(relative?, preceding_command, @point)
607
+ end
608
+
609
+ def to_concise_syntax_fragments
610
+ used_same_command? ? @point.y.to_s : (instructions_char + @point.y.to_s)
611
+ end
612
+
613
+ def instructions_char
614
+ relative? ? 'v' : 'V'
615
+ end
616
+ end
617
+
618
+ class CurveCommandBase < CommandBase #:nodoc:
619
+ def initialize(relative, preceding_command, *points)
620
+ raise ArgumentError, "wrong number of arguments (2 for #{pt_cnt + 2})" if points.size != pt_cnt
621
+ raise ArgumentError, 'preceding_command is nil' if preceding_command.nil?
622
+ @relative = relative
623
+ @preceding_command = preceding_command
624
+ @point = Coordinate.new(points.last)
625
+ @control_points = points[0..-2].map{|pt| Coordinate.new(pt)}
626
+ end
627
+
628
+ def last_control_point
629
+ relative? ? (preceding_point + @control_points.last) : @control_points.last
630
+ end
631
+
632
+ def to_concise_syntax_fragments
633
+ used_same_command? ? @point.to_s : (instructions_char + @point.to_s)
634
+ end
635
+
636
+ def to_concise_syntax_fragments
637
+ fragments = @control_points.map{|ctrl_pt| ctrl_pt.to_s}.push(@point.to_s)
638
+ fragments[0] = instructions_char + fragments[0] unless used_same_command?
639
+ fragments
640
+ end
641
+
642
+ private
643
+
644
+ def pt_cnt
645
+ self.class.pt_cnt
646
+ end
647
+
648
+ class << self
649
+ def commands(relative, preceding_command, *points)
650
+ raise ArgumentError, "number of points must be a multipule of #{pt_cnt}" if points.size % pt_cnt != 0
651
+ cmd = preceding_command
652
+ points.each_slice(pt_cnt).inject([]) do |cmds, pts|
653
+ cmds << (cmd = new(relative, cmd, *pts))
654
+ end
655
+ end
656
+ end
657
+ end
658
+
659
+ class CurveCommand < CurveCommandBase #:nodoc:
660
+ def preceding_control_point
661
+ if preceding_command.is_a?(CurveCommand)
662
+ preceding_command.last_control_point
663
+ else
664
+ preceding_command.last_point
665
+ end
666
+ end
667
+
668
+ def control_point1
669
+ @control_points[0]
670
+ end
671
+
672
+ def control_point2
673
+ @control_points[1]
674
+ end
675
+
676
+ def instructions_char
677
+ relative? ? 'c' : 'C'
678
+ end
679
+
680
+ class << self
681
+ def pt_cnt
682
+ 3
683
+ end
684
+ end
685
+ end
686
+
687
+ class ShorthandCurveCommand < CurveCommand #:nodoc:
688
+ def control_point1
689
+ if relative?
690
+ preceding_point - preceding_control_point
691
+ else
692
+ preceding_point * 2 - preceding_control_point
693
+ end
694
+ end
695
+
696
+ def control_point2
697
+ @control_points[0]
698
+ end
699
+
700
+ def to_compatible_commands(preceding_command)
701
+ CurveCommand.new(relative?, preceding_command, control_point1, control_point2, @point)
702
+ end
703
+
704
+ def instructions_char
705
+ relative? ? 's' : 'S'
706
+ end
707
+
708
+ class << self
709
+ def pt_cnt
710
+ 2
711
+ end
712
+ end
713
+ end
714
+
715
+ class QuadraticCurveCommand < CurveCommandBase #:nodoc:
716
+ def preceding_control_point
717
+ if preceding_command.is_a?(QuadraticCurveCommand)
718
+ preceding_command.last_control_point
719
+ else
720
+ preceding_command.last_point
721
+ end
722
+ end
723
+
724
+ def control_point
725
+ @control_points[0]
726
+ end
727
+
728
+ def to_compatible_commands(preceding_command)
729
+ ctrl_pt1 = relative? ? control_point * 2.0 / 3.0 : (preceding_point + control_point * 2.0) / 3.0
730
+ ctrl_pt2 = (control_point * 2.0 + point) / 3.0
731
+ CurveCommand.new(relative?, preceding_command, ctrl_pt1, ctrl_pt2, @point)
732
+ end
733
+
734
+ def instructions_char
735
+ relative? ? 'q' : 'Q'
736
+ end
737
+
738
+ class << self
739
+ def pt_cnt
740
+ 2
741
+ end
742
+ end
743
+ end
744
+
745
+ class ShorthandQuadraticCurveCommand < QuadraticCurveCommand #:nodoc:
746
+ def control_point
747
+ if relative?
748
+ preceding_point - preceding_control_point
749
+ else
750
+ preceding_point * 2 - preceding_control_point
751
+ end
752
+ end
753
+
754
+ def last_control_point
755
+ preceding_point * 2 - preceding_control_point
756
+ end
757
+
758
+ def instructions_char
759
+ relative? ? 't' : 'T'
760
+ end
761
+
762
+ class << self
763
+ def pt_cnt
764
+ 1
765
+ end
766
+ end
767
+ end
768
+
769
+ class ArcCommand < CommandBase #:nodoc:
770
+ attr_reader :rx, :ry, :rotation
771
+
772
+ def initialize(relative, preceding_command, rx, ry, rotation, is_large_arc, is_clockwise, point)
773
+ raise ArgumentError, 'preceding_command is nil' if preceding_command.nil?
774
+ @relative = relative
775
+ @preceding_command = preceding_command
776
+ @point = Coordinate.new(point)
777
+ @rotation = rotation
778
+ @is_large_arc = is_large_arc
779
+ @is_clockwise = is_clockwise
780
+ @rx = Length.new(rx).abs
781
+ @ry = Length.new(ry).abs
782
+ l = (modified_mid_point.x.to_f / @rx.to_f) ** 2 + (modified_mid_point.y.to_f / @ry.to_f) ** 2
783
+ if 1 < l
784
+ @rx *= Math.sqrt(l)
785
+ @ry *= Math.sqrt(l)
786
+ end
787
+ end
788
+
789
+ def large_arc?
790
+ @is_large_arc
791
+ end
792
+
793
+ def clockwise?
794
+ @is_clockwise
795
+ end
796
+
797
+ def to_compatible_commands(preceding_command)
798
+ return LineCommand.new(relative?, preceding_command, point) if rx.zero? || ry.zero?
799
+ division_count = (center_angle / 30.0).ceil
800
+ division_angle = center_angle / division_count * (clockwise? ? 1 : -1)
801
+ current_point = start_angle_point
802
+ compat_commands = []
803
+ division_count.times do |i|
804
+ end_point = if i == division_count - 1
805
+ end_angle_point
806
+ else
807
+ Matrix.rotate(division_angle).transform(current_point)
808
+ end
809
+ control_point1 = control_point_of_curve(current_point, division_angle, true)
810
+ control_point2 = control_point_of_curve(end_point, division_angle, false)
811
+ path_point = (i == division_count - 1) ? point : transform_orginal_shape(end_point)
812
+ if relative?
813
+ control_point1 += preceding_point
814
+ control_point2 += preceding_point
815
+ path_point += preceding_point
816
+ end
817
+ preceding_command = CurveCommand.absolute_commands(preceding_command,
818
+ control_point1,
819
+ control_point2,
820
+ path_point).first
821
+ compat_commands << preceding_command
822
+ current_point = end_point
823
+ end
824
+ compat_commands
825
+ end
826
+
827
+ def to_concise_syntax_fragments
828
+ [used_same_command? ? rx.to_s : instructions_char + rx.to_s,
829
+ ry, rotation, large_arc? ? 1 : 0, clockwise? ? 1 : 0, point.to_s]
830
+ end
831
+
832
+ def instructions_char
833
+ relative? ? 'a' : 'A'
834
+ end
835
+
836
+ def center_point
837
+ st_pt = relative? ? Coordinate::ZERO : preceding_point
838
+ Matrix.rotate(rotation).transform(modified_center_point) + (st_pt + point) * 0.5
839
+ end
840
+
841
+ private
842
+
843
+ def modified_mid_point
844
+ st_pt = relative? ? Coordinate::ZERO : preceding_point
845
+ Matrix.rotate(-rotation).transform((st_pt - point) * 0.5)
846
+ end
847
+
848
+ def modified_center_point
849
+ pt = modified_mid_point
850
+ Coordinate.new(pt.y * (rx / ry), -pt.x * (ry / rx)) *
851
+ Math.sqrt(((rx.to_f * ry.to_f) ** 2 - (rx.to_f * pt.y.to_f) ** 2 - (ry.to_f * pt.x.to_f) ** 2) /
852
+ ((rx.to_f * pt.y.to_f) ** 2 + (ry.to_f * pt.x.to_f) ** 2)) *
853
+ ((large_arc? == clockwise?) ? -1 : 1)
854
+ end
855
+
856
+ def start_angle_point
857
+ Coordinate.new((modified_mid_point.x - modified_center_point.x) / rx,
858
+ (modified_mid_point.y - modified_center_point.y) / ry)
859
+ end
860
+
861
+ def end_angle_point
862
+ Coordinate.new((-modified_mid_point.x - modified_center_point.x) / rx,
863
+ (-modified_mid_point.y - modified_center_point.y) / ry)
864
+ end
865
+
866
+ def center_angle
867
+ angle = Math.acos(start_angle_point.x.to_f * end_angle_point.x.to_f +
868
+ start_angle_point.y.to_f * end_angle_point.y.to_f) * 180.0 / Math::PI
869
+ large_arc? ? 360.0 - angle : angle
870
+ end
871
+
872
+ def transform_matrix
873
+ Matrix.translate(center_point.x.to_f, center_point.y.to_f).rotate(rotation).scale(rx.to_f, ry.to_f)
874
+ end
875
+
876
+ def transform_orginal_shape(modified_point)
877
+ transform_matrix.transform(modified_point)
878
+ end
879
+
880
+ def control_point_of_curve(point, center_angle, is_start_point)
881
+ handle_length = Math.tan(center_angle * Math::PI / 180.0 / 4.0) * 4.0 / 3.0
882
+ handle = is_start_point ? handle_length : -handle_length
883
+ transform_matrix.transform(Matrix.new(1, handle, -handle, 1, 0, 0).transform(point))
884
+ end
885
+
886
+ class << self
887
+ def commands(relative, preceding_command, *args)
888
+ raise ArgumentError, "number of arguments must be a multipule of 6" if args.size % 6 != 0
889
+ cmd = preceding_command
890
+ args.each_slice(6).inject([]) do |cmds, ars|
891
+ if ars[0].zero? || ars[1].zero?
892
+ cmds << (cmd = LineCommand.new(relative, cmd, ars.last))
893
+ else
894
+ cmds << (cmd = new(relative, cmd, *ars))
895
+ end
896
+ end
897
+ end
898
+ end
899
+ end
900
+ end
901
+ end
902
+ end