midinous 1.0.0.beta

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,1060 @@
1
+ # Copyright (C) 2019 James "Nornec" Ratliff
2
+ #
3
+ # This file is part of Midinous
4
+ #
5
+ # Midinous is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Midinous is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Midinous. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ class Point_Logic
19
+ include Logic_Controls
20
+
21
+ def initialize
22
+ @prop_names = ["Note",
23
+ "Velocity",
24
+ "Duration (beats)",
25
+ "Channel",
26
+ "Repeat",
27
+ "X-coordinate",
28
+ "Y-coordinate",
29
+ "Color",
30
+ "Path Mode",
31
+ "Signal Start",
32
+ "Play Mode"]
33
+ @prop_names_multi = ["Note",
34
+ "Velocity",
35
+ "Duration (beats)",
36
+ "Channel",
37
+ "Repeat",
38
+ "Color",
39
+ "Signal Start",
40
+ "Play Mode"]
41
+ @prop_names_adv = []
42
+ @prop_names_adv_multi = []
43
+ @curr_prop = nil
44
+ @curr_prop_adv = nil
45
+ @mempoints = []
46
+ @mempointsbuff = []
47
+ @copy_pos = []
48
+ @copy_type = nil
49
+ @paste_count = 0
50
+ end
51
+
52
+ def add_point(r_origin,points) #Point existence search
53
+ unless (collision_check(r_origin,points))
54
+ points << NousPoint.new(r_origin,-1)
55
+ end
56
+ return points
57
+ end
58
+
59
+ def add_path(points)
60
+ points.find_all { |n| n.pathable && !n.source}.each do |t|
61
+ points.find(&:source).path_to << t
62
+ t.path_from << points.find(&:source)
63
+ end
64
+ return points
65
+ end
66
+
67
+ def set_path_mode(mode)
68
+ CC.nouspoints.find_all(&:selected).each {|n| n.path_mode = mode}
69
+ UI::canvas.queue_draw
70
+ end
71
+ def set_note(note)
72
+ CC.nouspoints.find_all(&:selected).each {|n| n.note = note}
73
+ populate_prop(CC.nouspoints)
74
+ end
75
+ def inc_note(inc)
76
+ CC.nouspoints.find_all(&:selected).each do |n|
77
+ signed = n.note
78
+ val = n.note.to_i
79
+ val += inc
80
+ if val >= 0
81
+ n.note = "+#{val}"
82
+ else n.note = val
83
+ end
84
+ if !(signed.to_s.include?("+") || signed.to_s.include?("-"))
85
+ n.note = n.note.to_i
86
+ n.note = n.note.clamp(0,127)
87
+ end
88
+ end
89
+ populate_prop(CC.nouspoints)
90
+ end
91
+
92
+ def collision_check(r_origin,points)
93
+ return true if points.any? { |n| r_origin == n.origin }
94
+ end
95
+
96
+ def select_points(box,points) #Select points with the select tool
97
+ UI::prop_list_model.clear
98
+ UI::prop_mod.text = ""
99
+ box_origin = [box[0],box[1]] #box is an array with 4 values
100
+ points.each do |n|
101
+ if check_bounds(n.origin,box)
102
+ n.select
103
+ elsif check_bounds(box_origin,n.bounds)
104
+ n.select
105
+ else
106
+ n.deselect
107
+ UI::prop_list_model.clear
108
+ UI::prop_mod.text = ""
109
+ end
110
+ end
111
+ populate_prop(points)
112
+ return points
113
+ end
114
+
115
+ def select_all
116
+ CC.nouspoints.each {|n| n.selected = true} unless CC.nouspoints == []
117
+ populate_prop(CC.nouspoints)
118
+ UI::canvas.queue_draw
119
+ end
120
+ def copy_points (type)
121
+ return if CC.nouspoints.empty?
122
+ origins = []
123
+ CC.nouspoints.find_all(&:selected).each {|n| origins << n.origin}
124
+ @copy_pos = origins.min
125
+ @copy_type = type
126
+ @mempointsbuff = CC.nouspoints.find_all(&:selected)
127
+ end
128
+ def paste_points
129
+ return if @mempointsbuff.empty?
130
+ copy_lookup = {}
131
+
132
+ # Clone the points and track old point => new point
133
+ @mempointsbuff.each do |n|
134
+ n.selected = false if @copy_type == "copy"
135
+ mem_point = n.clone
136
+ @mempoints << mem_point
137
+ copy_lookup[n.object_id] = mem_point
138
+ end
139
+
140
+ # Update the point relations
141
+ @mempoints.each do |n|
142
+ n.path_to = n.path_to.map { |pt| copy_lookup[pt.object_id] }.compact
143
+ n.path_from = n.path_from.map { |pf| copy_lookup[pf.object_id] }.compact
144
+ end
145
+
146
+ @paste_count = 0 if @copy_type == "copy"
147
+ CC.nouspoints.reject!(&:selected) if @copy_type == "cut" && @paste_count == 0
148
+ @paste_count += 1
149
+
150
+ paste_pos = CC.mouse_last_pos
151
+ diff = round_to_grid([paste_pos[0] - @copy_pos[0],paste_pos[1] - @copy_pos[1]])
152
+ @mempoints.each {|m| m.set_destination(diff)}
153
+ @mempoints.each {|m| CC.nouspoints << m} if paste_check(diff,@mempoints)
154
+ @mempoints = []
155
+ populate_prop(CC.nouspoints)
156
+ UI::canvas.queue_draw
157
+ end
158
+ def paste_check(diff,memp)
159
+ memp.each {|m| return false if CC.nouspoints.any? { |n| n.origin == m.origin}}
160
+ return true
161
+ end
162
+
163
+ def populate_prop (points)
164
+ UI::prop_list_model.clear
165
+ UI::prop_mod.text = ""
166
+ point = nil
167
+ point = points.find(&:selected) if points.find_all(&:selected).length == 1
168
+ if point
169
+ prop_vals = [point.note,
170
+ point.velocity,
171
+ point.duration,
172
+ point.channel,
173
+ point.repeat,
174
+ point.x,
175
+ point.y,
176
+ color_to_hex(point.default_color),
177
+ point.path_mode,
178
+ point.traveler_start,
179
+ point.play_modes[0]]
180
+ @prop_names.each do |v|
181
+ iter = UI::prop_list_model.append
182
+ iter[0] = v
183
+ iter[1] = prop_vals[@prop_names.find_index(v)].to_s
184
+ end
185
+ elsif points.find_all(&:selected).length > 1
186
+ @prop_names_multi.each do |v|
187
+ equalizer = []
188
+ iter = UI::prop_list_model.append
189
+ iter[0] = v
190
+ case v
191
+ when "Note"
192
+ points.find_all(&:selected).each {|p| equalizer << p.note}
193
+ when "Velocity"
194
+ points.find_all(&:selected).each {|p| equalizer << p.velocity}
195
+ when "Duration (beats)"
196
+ points.find_all(&:selected).each {|p| equalizer << p.duration}
197
+ when "Channel"
198
+ points.find_all(&:selected).each {|p| equalizer << p.channel}
199
+ when "Color"
200
+ points.find_all(&:selected).each {|p| equalizer << color_to_hex(p.default_color)}
201
+ when "Signal Start"
202
+ points.find_all(&:selected).each {|p| equalizer << p.traveler_start}
203
+ when "Play Mode"
204
+ points.find_all(&:selected).each {|p| equalizer << p.play_modes[0]}
205
+ when "Repeat"
206
+ points.find_all(&:selected).each {|p| equalizer << p.repeat}
207
+ end
208
+ if equalizer.uniq.count == 1
209
+ iter[1] = equalizer[0].to_s
210
+ else iter[1] = "Multiple Values"
211
+ end
212
+ end
213
+ else
214
+ UI::prop_list_model.clear
215
+ UI::prop_mod.text = ""
216
+ end
217
+
218
+ end
219
+ def prop_list_select(selected)
220
+ return if selected == nil
221
+ @curr_prop = selected[0]
222
+ if selected[1][0] == "#"
223
+ UI::prop_mod.text = selected[1][1..6]
224
+ else UI::prop_mod.text = selected[1]
225
+ end
226
+ UI::prop_mod.position = 0
227
+ UI::prop_mod.grab_focus
228
+ end
229
+
230
+ def check_input(text)
231
+ play_modes = ["robin","split","portal","random"]
232
+ path_modes = ["horz","vert"]
233
+ signal_states = ["true","false"]
234
+ case @curr_prop
235
+ when "Note"
236
+ if (text.to_i >= 1 && text.to_i <= 127) || (text.match(/^([+]|-)[0-9]{1,2}$/))
237
+ UI::prop_mod_button.sensitive = true
238
+ else UI::prop_mod_button.sensitive = false
239
+ end
240
+ when "Velocity"
241
+ if (text.to_i >= 1 && text.to_i <= 127)
242
+ UI::prop_mod_button.sensitive = true
243
+ else UI::prop_mod_button.sensitive = false
244
+ end
245
+ when "Duration (beats)"
246
+ if text.to_i >= 1 && text.to_i <= 1000
247
+ UI::prop_mod_button.sensitive = true
248
+ else UI::prop_mod_button.sensitive = false
249
+ end
250
+ when "Channel"
251
+ if text.to_i >= 1 && text.to_i <= 16
252
+ UI::prop_mod_button.sensitive = true
253
+ else UI::prop_mod_button.sensitive = false
254
+ end
255
+ when "X-coordinate", "Y-coordinate"
256
+ if round_num_to_grid(text.to_i) >= CC.grid_spacing &&
257
+ round_num_to_grid(text.to_i) <= (CANVAS_SIZE - CC.grid_spacing)
258
+ then
259
+ UI::prop_mod_button.sensitive = true
260
+ else UI::prop_mod_button.sensitive = false
261
+ end
262
+ when "Color"
263
+ if text.match(/^[0-9A-Fa-f]{6}$/)
264
+ UI::prop_mod_button.sensitive = true
265
+ else UI::prop_mod_button.sensitive = false
266
+ end
267
+ when "Path Mode"
268
+ if path_modes.include? text
269
+ UI::prop_mod_button.sensitive = true
270
+ else UI::prop_mod_button.sensitive = false
271
+ end
272
+ when "Signal Start"
273
+ if signal_states.include? text
274
+ UI::prop_mod_button.sensitive = true
275
+ else UI::prop_mod_button.sensitive = false
276
+ end
277
+ when "Play Mode"
278
+ if play_modes.include? text
279
+ UI::prop_mod_button.sensitive = true
280
+ else UI::prop_mod_button.sensitive = false
281
+ end
282
+ when "Repeat"
283
+ if text.to_i >= 0 && text.to_i <= 128
284
+ UI::prop_mod_button.sensitive = true
285
+ else UI::prop_mod_button.sensitive = false
286
+ end
287
+ else UI::prop_mod_button.sensitive = false
288
+ end
289
+ end
290
+
291
+ def modify_properties(points)
292
+ case @curr_prop
293
+ when "Note"
294
+ points.find_all(&:selected).each do |p|
295
+ if UI::prop_mod.text.match(/^([+]|-)[0-9]{1,2}$/)
296
+ unless p.traveler_start == true
297
+ p.note = UI::prop_mod.text
298
+ p.use_rel = true
299
+ end
300
+ elsif (UI::prop_mod.text.to_i >= 0 &&
301
+ UI::prop_mod.text.to_i <= 127 &&
302
+ !(UI::prop_mod.text.include?("+") || UI::prop_mod.text.include?("-")))
303
+ then
304
+ p.note = UI::prop_mod.text.to_i
305
+ p.use_rel = false
306
+ end
307
+ end
308
+ when "Velocity"
309
+ points.find_all(&:selected).each {|p| p.velocity = UI::prop_mod.text.to_i}
310
+ when "Duration (beats)"
311
+ points.find_all(&:selected).each {|p| p.duration = UI::prop_mod.text.to_i}
312
+ when "Channel"
313
+ points.find_all(&:selected).each {|p| p.channel = UI::prop_mod.text.to_i}
314
+ when "X-coordinate"
315
+ points.find(&:selected).x = UI::prop_mod.text.to_i
316
+ when "Y-coordinate"
317
+ points.find(&:selected).y = UI::prop_mod.text.to_i
318
+ when "Color"
319
+ points.find_all(&:selected).each {|p| p.set_default_color(hex_to_color("##{UI::prop_mod.text}"))}
320
+ when "Path Mode"
321
+ points.find(&:selected).path_mode = UI::prop_mod.text
322
+ when "Signal Start"
323
+ case UI::prop_mod.text
324
+ when "true"
325
+ points.find_all(&:selected).each {|p| p.traveler_start = true}
326
+ when "false"
327
+ points.find_all(&:selected).each {|p| p.traveler_start = false}
328
+ end
329
+ when "Play Mode"
330
+ if UI::prop_mod.text == "robin" || UI::prop_mod.text == "portal"
331
+ points.find_all(&:selected).each {|p| p.play_modes.rotate! until p.play_modes[0] == UI::prop_mod.text}
332
+ else
333
+ points.find_all {|p| p.selected == true && p.path_to.length > 1}.each {|p| p.play_modes.rotate! until p.play_modes[0] == UI::prop_mod.text}
334
+ end
335
+ when "Repeat"
336
+ points.find_all(&:selected).each do |p|
337
+ p.repeat = UI::prop_mod.text.to_i
338
+ end
339
+ end
340
+ return points
341
+ end
342
+
343
+ def select_path_point(origin,points,source_chosen)
344
+ points.find_all {|g| check_bounds(origin,g.bounds)}.each do |n|
345
+ case !n.pathable
346
+ when true #If clicking where a non-pathable point is
347
+ source_chosen = n.path_set(source_chosen)
348
+ when false
349
+ if n.source
350
+ points, source_chosen = cancel_path(points)
351
+ end
352
+ n.path_unset
353
+ end
354
+ end
355
+ return points, source_chosen
356
+ end
357
+
358
+ def play_mode_rotate(dir)
359
+ CC.nouspoints.find_all(&:selected).each do |n|
360
+ if n.path_to.length <= 1
361
+ case n.play_modes[0]
362
+ when "robin"
363
+ n.play_modes.rotate!(dir) until n.play_modes[0] == "portal"
364
+ when "portal"
365
+ n.play_modes.rotate!(dir) until n.play_modes[0] == "robin"
366
+ end
367
+ else
368
+ n.play_modes.rotate!(dir)
369
+ end
370
+ UI::canvas.queue_draw
371
+ end
372
+ end
373
+ def play_mode_rotate_selected(dir)
374
+ CC.nouspoints.find_all(&:selected).each do |n|
375
+ case dir
376
+ when "+"
377
+ n.path_to.rotate!(1)
378
+ when "-"
379
+ n.path_to.rotate!(-1)
380
+ end
381
+ end
382
+ UI::canvas.queue_draw
383
+ end
384
+
385
+ def cancel_selected(points)
386
+ points.find_all(&:selected).each { |n| n.deselect }
387
+ return points
388
+ end
389
+ def cancel_path(points)
390
+ points.find_all(&:pathable).each { |n| n.path_unset }
391
+ return points, false
392
+ end
393
+
394
+ def delete_points(points)
395
+ points.find_all {|f| !f.path_to.length.zero?}.each {|n| n.path_to.reject!(&:selected)}
396
+ points.find_all {|f| !f.path_from.length.zero?}.each {|n| n.path_from.reject!(&:selected)}
397
+ points.reject!(&:selected)
398
+ UI::prop_list_model.clear
399
+ UI::prop_mod.text = ""
400
+ return points
401
+ end
402
+ def delete_paths_to(points)
403
+ points.find_all {|f| !f.path_to.length.zero? && f.selected == true}.each {|n| n.path_to.each {|b| b.path_from.reject! {|g| g == n }}}
404
+ points.find_all {|f| !f.path_to.length.zero? && f.selected == true}.each {|n| n.path_to = []}
405
+ UI::canvas.queue_draw
406
+ end
407
+ def delete_paths_from(points)
408
+ points.find_all {|f| !f.path_from.length.zero? && f.selected == true}.each {|n| n.path_from.each {|b| b.path_to.reject! {|g| g == n }}}
409
+ points.find_all {|f| !f.path_from.length.zero? && f.selected == true}.each {|n| n.path_from = []}
410
+ UI::canvas.queue_draw
411
+ end
412
+
413
+ def move_points(diff,points)
414
+ if move_check(diff,points)
415
+ points.find_all(&:selected).each {|n| n.set_destination(diff) }
416
+ populate_prop(points)
417
+ end
418
+ return points
419
+ end
420
+ def move_check(diff,points)
421
+ points.find_all(&:selected).each do |n|
422
+ dest = n.origin.map
423
+ dest = dest.to_a
424
+ dest.map! {|g| g += diff[dest.find_index(g)]}
425
+ return false if points.find_all(&:not_selected).any? { |g| g.origin == dest}
426
+ end
427
+ return true
428
+ end
429
+
430
+ def set_start
431
+ CC.nouspoints.find_all(&:selected).each do |n|
432
+ if n.traveler_start == false
433
+ n.traveler_start = true
434
+ elsif n.traveler_start == true
435
+ n.traveler_start = false
436
+ end
437
+ end
438
+ UI::canvas.queue_draw
439
+ populate_prop(CC.nouspoints)
440
+ end
441
+
442
+ end
443
+
444
+ class NousPoint
445
+ include Logic_Controls
446
+ attr_accessor :source, :color, :path_to, :path_from, :note, :x, :y,
447
+ :velocity, :duration, :default_color, :path_mode,
448
+ :traveler_start, :channel, :playing, :play_modes,
449
+ :path_to_memory, :repeat, :repeating,
450
+ :use_rel, :selected, :save_id, :path_to_rels, :path_from_rels
451
+ attr_reader :pathable, :origin, :bounds
452
+
453
+ def initialize(o,save_id) #where the point was initially placed
454
+ @dp = [4,8,10,12,14,16,20]
455
+
456
+ @x = o[0]
457
+ @y = o[1]
458
+ @origin = o
459
+ @bounds = [@x-@dp[5],@y-@dp[5],@x+@dp[5],@y+@dp[5]]
460
+ @color = GREY #point color defaults to gray++
461
+ @path_color = CYAN
462
+ @default_color = GREY
463
+ @note = 60 #all notes start at middle c (C3), can be a note or a reference
464
+ @velocity = 100 # `` with 100 velocity
465
+ @channel = 1 # `` assigned to midi channel 1 (instrument 1, but we will refer to them as channels, not instruments)
466
+ @duration = 1 #length of note in grid points (should be considered beats)
467
+ @repeat = 0 #Number of times the node should repeat before moving on
468
+ @save_id = save_id
469
+ @play_modes = ["robin","split","portal","random"]
470
+ @traveler_start = false
471
+ @playing = false
472
+ @pathable = false
473
+ @selected = false
474
+ @source = false
475
+ @repeating = false
476
+ @use_rel = false
477
+ @path_to = [] #array of references to points that are receiving a path from this point
478
+ @path_to_memory = [] #memory of @path_to so that it can be reset upon stopping.
479
+ @path_to_rels = []
480
+ @path_from_rels = []
481
+ @path_from = [] #array of references to points that are sending a path to this point
482
+ @path_mode = "horz"
483
+ @chev_offsets = [0,0,0,0]
484
+ end
485
+
486
+ def set_rels
487
+ @path_to_rels = []
488
+ @path_from_rels = []
489
+ path_to.each {|pt| @path_to_rels << pt.save_id}
490
+ path_from.each {|pf| @path_from_rels << pf.save_id}
491
+ end
492
+
493
+ def write_props(file)
494
+ set_rels
495
+ file.write("#{@save_id}<~>")
496
+ file.write("#{@origin}<~>")
497
+ file.write("#{@note}<~>")
498
+ file.write("#{@velocity}<~>")
499
+ file.write("#{@channel}<~>")
500
+ file.write("#{@duration}<~>")
501
+ file.write("#{color_to_hex(@default_color)}<~>")
502
+ file.write("#{@repeat}<~>")
503
+ file.write("#{@play_modes}<~>")
504
+ file.write("#{@traveler_start}<~>")
505
+ file.write("#{@use_rel}<~>")
506
+ file.write("#{@path_mode}<~>")
507
+ file.write("#{@path_to_rels}<~>")
508
+ file.write("#{@path_from_rels}")
509
+ end
510
+ def read_props(file)
511
+ end
512
+
513
+ def not_selected
514
+ !@selected
515
+ end
516
+ def not_pathable
517
+ !@pathable
518
+ end
519
+
520
+ def origin=(o) #sets the origin of the point explicitly
521
+ @x = o[0]
522
+ @y = o[1]
523
+ @origin = o
524
+ @bounds = [@x-@dp[5],@y-@dp[5],@x+@dp[5],@y+@dp[5]]
525
+ end
526
+
527
+ def path_set(source_chosen)
528
+ @pathable = true
529
+ case source_chosen
530
+ when false #if source point was not chosen (first point clicked on path screen)
531
+ @source = true #Path source is now chosen on this node
532
+ @color = CYAN
533
+ return true
534
+ when true #Path source is already chosen in this operation
535
+ @color = GREEN
536
+ end
537
+ return source_chosen
538
+ end
539
+ def path_unset
540
+ @pathable = false
541
+ @source = false
542
+ @color = @default_color
543
+ end
544
+
545
+ def reset_path_to
546
+ if @path_to.length != @path_to_memory.length
547
+ @path_to_memory = []
548
+ UI.confirm("path_warning")
549
+ else
550
+ @path_to = []
551
+ @path_to_memory.each {|p| @path_to << p}
552
+ @path_to_memory = []
553
+ end
554
+ end
555
+
556
+ def set_default_color(c)
557
+ @color = c
558
+ @default_color = c
559
+ end
560
+ def set_destination(diff) #sets a new origin for the point based on x,y coordinate differences
561
+ @x += diff[0]
562
+ @y += diff[1]
563
+ @origin = [@x,@y]
564
+ @bounds = [@x-@dp[5],@y-@dp[5],@x+@dp[5],@y+@dp[5]]
565
+ end
566
+ def select #elevate color to denote 'selected' and sets a flag
567
+ @selected = true
568
+ end
569
+ def deselect #resets the color from elevated 'selected' values and sets a flag
570
+ @selected = false
571
+ @color = @default_color
572
+ end
573
+
574
+ def draw(cr) #point will always be drawn to this specification.
575
+ cr.set_source_rgba(@color[0],@color[1],@color[2],0.4)
576
+ if @traveler_start
577
+ traveler_start_draw(cr)
578
+ else
579
+ cr.rounded_rectangle(@x-@dp[1],@y-@dp[1],@dp[5],@dp[5],2,2) #slightly smaller rectangle adds 'relief' effect
580
+ end
581
+ cr.fill
582
+
583
+ cr.set_source_rgba(@color[0],@color[1],@color[2],1)
584
+ case @play_modes[0]
585
+ when "robin"
586
+ @path_color = CYAN
587
+ if @path_to.length > 1
588
+ cr.move_to(@x-8,@y)
589
+ cr.line_to(@x+6,@y-9)
590
+ cr.set_line_width(2)
591
+ cr.stroke
592
+ cr.move_to(@x-8,@y)
593
+ cr.set_dash([1,4],0)
594
+ cr.line_to(@x+6,@y+9)
595
+ cr.set_line_width(2)
596
+ cr.stroke
597
+ cr.set_dash([],0)
598
+ else
599
+ cr.circle(@x,@y,1)
600
+ cr.fill
601
+ end
602
+ when "split"
603
+ @path_color = CYAN
604
+ cr.move_to(@x-8,@y)
605
+ cr.line_to(@x+6,@y-9)
606
+ cr.move_to(@x-8,@y)
607
+ cr.line_to(@x+6,@y+9)
608
+ cr.set_line_width(2)
609
+ cr.stroke
610
+ when "portal"
611
+ @path_color = RED
612
+ cr.circle(@x,@y,6)
613
+ cr.set_line_width(2)
614
+ cr.stroke
615
+ when "random"
616
+ @path_color = VLET
617
+ cr.rectangle(@x-6,@y-2,8,8)
618
+ cr.rectangle(@x-2,@y-6,8,8)
619
+ cr.set_line_width(2)
620
+ cr.stroke
621
+ end
622
+
623
+ if @repeat > 0
624
+ #cr.move_to(@x-@dp[2],@y-@dp[2]) #top left of the point graphic
625
+ if @traveler_start
626
+ cr.move_to(@x+3,@y-@dp[2]-2)
627
+ cr.line_to(@x-2,-6+@y-@dp[2]-2)
628
+ cr.line_to(@x-2,6+@y-@dp[2]-2)
629
+ cr.line_to(@x+3,@y-@dp[2]-2)
630
+ cr.fill
631
+ cr.move_to(@x-3,@y+@dp[2]+2)
632
+ cr.line_to(@x+2,-6+@y+@dp[2]+2)
633
+ cr.line_to(@x+2,6+@y+@dp[2]+2)
634
+ cr.line_to(@x-3,@y+@dp[2]+2)
635
+ cr.fill
636
+ else
637
+ cr.move_to(@x-2,@y-@dp[2])
638
+ cr.line_to(@x-7,-6+@y-@dp[2])
639
+ cr.line_to(@x-7,6+@y-@dp[2])
640
+ cr.line_to(@x-2,@y-@dp[2])
641
+ cr.fill
642
+ cr.move_to(@x+2,@y+@dp[2])
643
+ cr.line_to(@x+7,-6+@y+@dp[2])
644
+ cr.line_to(@x+7,6+@y+@dp[2])
645
+ cr.line_to(@x+2,@y+@dp[2])
646
+ cr.fill
647
+ end
648
+ end
649
+
650
+ if !@selected
651
+ if @traveler_start
652
+ traveler_start_draw(cr)
653
+ else
654
+ cr.rounded_rectangle(@x-@dp[2],@y-@dp[2],@dp[6],@dp[6],2,2)
655
+ end
656
+ end
657
+ if @selected
658
+ cr.set_source_rgba(1,1,1,0.8)
659
+ if @traveler_start
660
+ traveler_start_draw(cr)
661
+ else
662
+ cr.rounded_rectangle(@x-@dp[2],@y-@dp[2],@dp[6],@dp[6],2,2) #a slightly smaller rectangle adds 'relief' effect
663
+ end
664
+ selection_caret_draw(cr)
665
+ end
666
+ cr.set_line_width(2)
667
+ cr.stroke
668
+ play_draw(cr) if @playing
669
+ repeat_draw(cr) if @repeating
670
+ end
671
+
672
+ def path_draw(cr)
673
+ if !@selected
674
+
675
+ cr.set_source_rgba(@path_color[0],@path_color[1],@path_color[2],0.6)
676
+ @path_to.each {|t| trace_path_to(cr,t)}
677
+
678
+ elsif @selected
679
+
680
+ cr.set_source_rgba(LGRN[0],LGRN[1],LGRN[2],0.8)
681
+ @path_to.each {|t| trace_path_to(cr,t)}
682
+ cr.set_line_cap(1) #Round
683
+ cr.set_line_join(2) #Miter
684
+ cr.set_line_width(3)
685
+ cr.stroke
686
+
687
+ end
688
+ end
689
+ def caret_draw(cr)
690
+ cr.set_dash([],0)
691
+ @chev_offsets = [0,0,0,0]
692
+ @path_from.each do |s|
693
+ input_mark_draw(cr,relative_pos(@x-s.x,@y-s.y),s)
694
+ cr.set_source_rgba(s.color[0],s.color[1],s.color[2],1)
695
+ cr.fill
696
+ end
697
+ end
698
+ def play_draw(cr) #If a note is playing, show a visual indicator
699
+ cr.set_source_rgb(@color[0],@color[1],@color[2])
700
+ if @traveler_start
701
+ traveler_start_draw(cr)
702
+ else
703
+ cr.rounded_rectangle(@x-@dp[2],@y-@dp[2],@dp[6],@dp[6],2,2)
704
+ end
705
+ cr.fill
706
+ end
707
+ def repeat_draw(cr)
708
+ cr.set_source_rgb(@color[0],@color[1],@color[2])
709
+ cr.rounded_rectangle(@x-@dp[2]+3,@y-@dp[2]+3,@dp[6]-6,@dp[6]-6,2,2)
710
+ cr.fill
711
+ end
712
+
713
+ def traveler_start_draw(cr) #Shape of a traveler start position
714
+ cr.move_to(@x-@dp[0],@y-@dp[3])
715
+ cr.line_to(@x+@dp[0],@y-@dp[3])
716
+ cr.line_to(@x+@dp[1],@y-@dp[1])
717
+ cr.line_to(@x+@dp[1],@y+@dp[1])
718
+ cr.line_to(@x+@dp[0],@y+@dp[3])
719
+ cr.line_to(@x-@dp[0],@y+@dp[3])
720
+ cr.line_to(@x-@dp[1],@y+@dp[1])
721
+ cr.line_to(@x-@dp[1],@y-@dp[1])
722
+ cr.line_to(@x-@dp[0],@y-@dp[3])
723
+ end
724
+ def selection_caret_draw(cr) #Shape of a selection caret
725
+ cr.move_to(@x-@dp[4],@y-@dp[4])
726
+ cr.line_to(@x-@dp[2],@y-@dp[4])
727
+ cr.move_to(@x-@dp[4],@y-@dp[4])
728
+ cr.line_to(@x-@dp[4],@y-@dp[2])
729
+
730
+ cr.move_to(@x+@dp[4],@y-@dp[4])
731
+ cr.line_to(@x+@dp[2],@y-@dp[4])
732
+ cr.move_to(@x+@dp[4],@y-@dp[4])
733
+ cr.line_to(@x+@dp[4],@y-@dp[2])
734
+
735
+ cr.move_to(@x-@dp[4],@y+@dp[4])
736
+ cr.line_to(@x-@dp[2],@y+@dp[4])
737
+ cr.move_to(@x-@dp[4],@y+@dp[4])
738
+ cr.line_to(@x-@dp[4],@y+@dp[2])
739
+
740
+ cr.move_to(@x+@dp[4],@y+@dp[4])
741
+ cr.line_to(@x+@dp[2],@y+@dp[4])
742
+ cr.move_to(@x+@dp[4],@y+@dp[4])
743
+ cr.line_to(@x+@dp[4],@y+@dp[2])
744
+ end
745
+ def trace_path_to(cr,t)
746
+
747
+ case @play_modes[0]
748
+ when "robin"
749
+ cr.set_dash([5,5],0)
750
+ if @path_to[0] == t
751
+ cr.set_dash([],0)
752
+ end
753
+ when "portal"
754
+ cr.set_dash([1,5],0)
755
+ end
756
+
757
+ rel_pos = relative_pos(t.x-@x,t.y-@y)
758
+ case rel_pos
759
+ when "n"
760
+ cr.move_to(@x,@y-10)
761
+ cr.line_to(t.x,t.y+10)
762
+ when "s"
763
+ cr.move_to(@x,@y+10)
764
+ cr.line_to(t.x,t.y-10)
765
+ when "e"
766
+ cr.move_to(@x+10,@y)
767
+ cr.line_to(t.x-10,t.y)
768
+ when "w"
769
+ cr.move_to(@x-10,@y)
770
+ cr.line_to(t.x+10,t.y)
771
+ end
772
+
773
+ case @path_mode
774
+ when "horz"
775
+ case rel_pos
776
+ when "ne"
777
+ cr.move_to(@x+10,@y)
778
+ cr.line_to(t.x,@y)
779
+ cr.line_to(t.x,t.y+10)
780
+ when "nw"
781
+ cr.move_to(@x-10,@y)
782
+ cr.line_to(t.x,@y)
783
+ cr.line_to(t.x,t.y+10)
784
+ when "se"
785
+ cr.move_to(@x+10,@y)
786
+ cr.line_to(t.x,@y)
787
+ cr.line_to(t.x,t.y-10)
788
+ when "sw"
789
+ cr.move_to(@x-10,@y)
790
+ cr.line_to(t.x,@y)
791
+ cr.line_to(t.x,t.y-10)
792
+ end
793
+ when "vert"
794
+ case rel_pos
795
+ when "ne"
796
+ cr.move_to(@x,@y-10)
797
+ cr.line_to(@x,t.y)
798
+ cr.line_to(t.x-10,t.y)
799
+ when "nw"
800
+ cr.move_to(@x,@y-10)
801
+ cr.line_to(@x,t.y)
802
+ cr.line_to(t.x+10,t.y)
803
+ when "se"
804
+ cr.move_to(@x,@y+10)
805
+ cr.line_to(@x,t.y)
806
+ cr.line_to(t.x-10,t.y)
807
+ when "sw"
808
+ cr.move_to(@x,@y+10)
809
+ cr.line_to(@x,t.y)
810
+ cr.line_to(t.x+10,t.y)
811
+ end
812
+ when "line"
813
+ cr.move_to(@x,@y)
814
+ cr.line_to(t.x,t.y)
815
+ end
816
+ cr.set_line_cap(1) #Round
817
+ cr.set_line_join(2) #Miter
818
+ cr.set_line_width(3)
819
+ cr.stroke
820
+ end
821
+
822
+ def trace_path_from(cr,s)
823
+ case s.path_mode
824
+ when "horz"
825
+ cr.move_to(s.x,s.y)
826
+ cr.line_to(@x,s.y)
827
+ cr.line_to(@x,@y)
828
+ when "vert"
829
+ cr.move_to(s.x,s.y)
830
+ cr.line_to(s.x,@y)
831
+ cr.line_to(@x,@y)
832
+ when "line"
833
+ cr.move_to(s.x,s.y)
834
+ cr.line_to(@x,@y)
835
+ end
836
+ end
837
+
838
+ def input_mark_draw(cr,rel_pos,s)
839
+ if s.path_mode == "horz"
840
+ case rel_pos
841
+ when "n","ne","nw"
842
+ draw_chevron(cr,@chev_offsets[0],"n",self)
843
+ @chev_offsets[0] += 5
844
+ when "s","se","sw"
845
+ draw_chevron(cr,@chev_offsets[1],"s",self)
846
+ @chev_offsets[1] += 5
847
+ when "e"
848
+ draw_chevron(cr,@chev_offsets[2],"e",self)
849
+ @chev_offsets[2] += 5
850
+ when "w"
851
+ draw_chevron(cr,@chev_offsets[3],"w",self)
852
+ @chev_offsets[3] += 5
853
+ end
854
+ elsif s.path_mode == "vert"
855
+ case rel_pos
856
+ when "n"
857
+ draw_chevron(cr,@chev_offsets[0],"n",self)
858
+ @chev_offsets[0] += 5
859
+ when "s"
860
+ draw_chevron(cr,@chev_offsets[1],"s",self)
861
+ @chev_offsets[1] += 5
862
+ when "e","ne","se"
863
+ draw_chevron(cr,@chev_offsets[2],"e",self)
864
+ @chev_offsets[2] += 5
865
+ when "w","nw","sw"
866
+ draw_chevron(cr,@chev_offsets[3],"w",self)
867
+ @chev_offsets[3] += 5
868
+ end
869
+ end
870
+ end
871
+
872
+ end
873
+
874
+ class Traveler #A traveler handles the source note playing and creates another traveler if the destination is reached.
875
+ attr_reader :remove, :dest, :dest_origin, :last_played_note, :played_note
876
+ attr_accessor :reached
877
+ def initialize(srce,dest,lpn) #Traveler should play note when reaches destination. Should not need to create another traveler if it's a dead end.
878
+ @srce = srce
879
+ @dest = dest
880
+ @srce_origin = srce.origin
881
+ @dest_origin = dest.origin
882
+ @repeat = dest.repeat
883
+ @travel_c = 0
884
+ @iteration = 0
885
+ @last_played_note = lpn
886
+ @played_note = nil
887
+ @distance = ((@dest_origin[0] - @srce_origin[0]).abs + (@dest_origin[1] - @srce_origin[1]).abs)/CC.grid_spacing
888
+ @reached = false
889
+ @remove = false
890
+ end
891
+
892
+ def travel
893
+ @travel_c += 1
894
+ if @travel_c == @distance
895
+ @dest.playing = true
896
+ @reached = true
897
+ play_check(@srce.use_rel,@dest.use_rel)
898
+ elsif @travel_c == (@distance + @dest.duration)
899
+ @dest.playing = false
900
+ CC.repeaters << Repeater.new(@dest,@repeat,@played_note) if @repeat > 0
901
+ queue_removal
902
+ end
903
+ end
904
+
905
+ def play_check(srce_rel,dest_rel)
906
+ if !dest_rel
907
+ @played_note = @dest.note
908
+ play_note
909
+ elsif srce_rel && dest_rel
910
+ @played_note = @last_played_note
911
+ play_relative
912
+ elsif !srce_rel && dest_rel
913
+ @played_note = @srce.note.to_i
914
+ play_relative
915
+ end
916
+ end
917
+ def play_note
918
+ queued_notes = []
919
+ CC.queued_note_plays.each {|q| queued_notes << [q.note,q.chan]}
920
+ CC.queued_note_plays << NoteSender.new(@played_note,@dest.channel,@dest.velocity) unless queued_notes.find {|f| @played_note == f[0] && @dest.channel == f[1]}
921
+ end
922
+ def play_relative
923
+ #search the current scales variable and round to nearest note.
924
+ rel = @dest.note.to_i
925
+ pn = @played_note
926
+ if @dest.note.to_s.include?("+")
927
+ (pn..127).each do |s|
928
+ rel -= 1 if CC.scale_posns[s] == true && s != pn
929
+ if rel == 0
930
+ pn = s
931
+ break
932
+ end
933
+ end
934
+ elsif @dest.note.to_s.include?("-")
935
+ (0..pn).reverse_each do |s|
936
+ rel += 1 if CC.scale_posns[s] == true && s != pn
937
+ if rel == 0
938
+ pn = s
939
+ break
940
+ end
941
+ end
942
+ end
943
+ @played_note = pn
944
+ play_note
945
+ end
946
+
947
+ def queue_removal
948
+ CC.queued_note_stops << NoteSender.new(@played_note,@dest.channel,0)
949
+
950
+ @remove = true
951
+ end
952
+
953
+ end
954
+
955
+ class Starter #A starter handles notes that are used as starting points for paths and point jumps for portals
956
+ attr_reader :remove, :last_played_note
957
+ def initialize(portal_srce,srce,lpn)
958
+ @travel_c = 0
959
+ @srce = srce
960
+ @portal_srce = portal_srce
961
+ @duration = srce.duration
962
+ @remove = false
963
+ @repeat = srce.repeat
964
+ @played_note = nil
965
+ @last_played_note = lpn
966
+
967
+ @srce.playing = true
968
+ if @portal_srce != nil
969
+ play_check(@portal_srce.use_rel,@srce.use_rel)
970
+ else
971
+ @played_note = @srce.note
972
+ play_note
973
+ end
974
+ end
975
+
976
+ def travel
977
+ @travel_c += 1
978
+ if @travel_c == @duration
979
+ @srce.playing = false
980
+ CC.queued_note_stops << NoteSender.new(@played_note,@srce.channel,0)
981
+ CC.repeaters << Repeater.new(@srce,@repeat,@played_note) if @repeat > 0
982
+ @remove = true
983
+ end
984
+ end
985
+
986
+ def play_check(portal_srce_rel,srce_rel)
987
+ if !srce_rel
988
+ @played_note = @srce.note
989
+ play_note
990
+ elsif portal_srce_rel && srce_rel
991
+ @played_note = @last_played_note
992
+ play_relative
993
+ elsif !portal_srce_rel && srce_rel
994
+ @played_note = @portal_srce.note.to_i
995
+ play_relative
996
+ end
997
+ end
998
+ def play_note
999
+ queued_notes = []
1000
+ CC.queued_note_plays.each {|q| queued_notes << [q.note,q.chan]}
1001
+ CC.queued_note_plays << NoteSender.new(@played_note,@srce.channel,@srce.velocity) unless queued_notes.find {|f| @played_note == f[0] && @srce.channel == f[1]}
1002
+ end
1003
+ def play_relative
1004
+ #search the current scales variable and round to nearest note.
1005
+ rel = @srce.note.to_i
1006
+ pn = @played_note
1007
+ if @srce.note.to_s.include?("+")
1008
+ (pn..127).each do |s|
1009
+ rel -= 1 if CC.scale_posns[s] == true && s != pn
1010
+ if rel == 0
1011
+ pn = s
1012
+ break
1013
+ end
1014
+ end
1015
+ elsif @srce.note.to_s.include?("-")
1016
+ (0..pn).reverse_each do |s|
1017
+ rel += 1 if CC.scale_posns[s] == true && s != pn
1018
+ if rel == 0
1019
+ pn = s
1020
+ break
1021
+ end
1022
+ end
1023
+ end
1024
+ @played_note = pn
1025
+ play_note
1026
+ end
1027
+
1028
+ end
1029
+
1030
+ class Repeater #A repeater handles notes that are set to repeat/arpeggiate. This logic is currently FUCKED
1031
+ attr_reader :remove
1032
+ def initialize(srce,count,played_note)
1033
+ @srce = srce
1034
+ @dur = srce.duration
1035
+ @timer = (count * @dur)
1036
+ @played_note = played_note
1037
+ end
1038
+
1039
+ def repeat
1040
+ if @timer == 0
1041
+ CC.queued_note_stops << NoteSender.new(@played_note,@srce.channel,0)
1042
+ @srce.repeating = false
1043
+ @remove = true
1044
+ end
1045
+ unless @remove == true
1046
+ @srce.repeating = true
1047
+ play_note if @timer % @dur == 0
1048
+ end
1049
+ @timer -= 1
1050
+ end
1051
+
1052
+ def play_note
1053
+ queued_notes = []
1054
+ CC.queued_note_plays.each {|q| queued_notes << [q.note,q.chan]}
1055
+ CC.queued_note_plays << NoteSender.new(@played_note,@srce.channel,@srce.velocity) unless queued_notes.find {|f| @played_note == f[0] && @srce.channel == f[1]}
1056
+ end
1057
+
1058
+ end
1059
+
1060
+ Pl = Point_Logic.new