-
Notifications
You must be signed in to change notification settings - Fork 0
/
workspace.moon
579 lines (524 loc) · 16.9 KB
/
workspace.moon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
export love, util, vec2, aabb2, ltable, lmath, option
buffer = require 'buffer'
cam11 = require 'cam11'
lg = love.graphics
class Workspace
gridSize: vec2 64, 64
infoFont: lg.newFont 'font/Quicksand-SemiBold.ttf', 22
panelFont: lg.newFont 'font/Quicksand-SemiBold.ttf', 20
new: (@sampleRate, @channelCount, @bitDepth = 16, @bufferCount = 4, @bufferSize = 1024) =>
@generation = 0
@lastGeneration = @generation - 1
-- @source = love.audio.newQueueableSource sampleRate, bitDepth, channelCount, bufferCount
-- @source\setVolume 0.1
@modules = {}
@mode = { kind: 'none' }
@rmode = { kind: 'none' }
@cam = cam11!
@zoom = 0
@cam\setZoom @getZoomAmount!
@outputPreBuffer = buffer.new bufferSize
@outputBuffer = love.sound.newSoundData bufferSize, sampleRate, bitDepth, channelCount
@outputSource = love.audio.newQueueableSource sampleRate, bitDepth, channelCount, bufferCount
@snapping = true
-- @moduleNames = ltable.imap love.filesystem.getDirectoryItems'module', (filename) -> (filename\gsub '%.[^%.]+$', '')
@moduleNames = [(item\gsub '%.[^%.]+$', '') for item in *love.filesystem.getDirectoryItems'module']
@panelOpen = false
@panelWidth = 320
@panelOffset = 0
update: (dt) =>
for module in *@modules
module\update dt
wpos = @getMousePos!
@hoveredSocket = @getSocketAtPos wpos
@hoveredModule = unless @hoveredSocket
@getModuleAtPos wpos
else nil
if @mode.kind == 'position'
for i, module in ipairs @mode.modules
delta = @mode.fromPoint\delta wpos
pos = @mode.fromPoss[i] + delta
module.pos = pos
module\snapIfNeeded!
if @mode.kind == 'select'
@clearSelected!
for module in *@modules
if @isModuleInSelection module
module.selected = true
if @dragInfo
delta = wpos\delta @dragInfo.fromPoint
@cam\setPos (delta + vec2 @cam\getPos!)\split!
while true
-- Only process if not processed already for this generation
if @generation != @lastGeneration
fromBuf = @outputPreBuffer
toBuf = @outputBuffer
-- Process modules
buffer.zero fromBuf, @bufferSize
for module in *@modules
module\process!
-- Take from sound output modules
for i = 0, @bufferSize - 1
toBuf\setSample i, fromBuf[i]
@lastGeneration = @generation
-- Try to queue up the buffer
status = @outputSource\queue @outputBuffer
@outputSource\play! unless @outputSource\isPlaying!
break unless status
@generation += 1
draw: =>
wpos = @getMousePos!
lg.clear 0.1, 0.1, 0.12, 1
lg.setLineStyle 'rough'
lg.setLineJoin 'bevel'
@cam\attach!
lg.push 'all'
-- Draw buffer of hovered module
if @hoveredSocket
buf = if @hoveredSocket[3]
@hoveredSocket[1]\getInput @hoveredSocket[2]
else
@hoveredSocket[1]\getOutput @hoveredSocket[2]
lg.push 'all'
lg.origin!
line = {}
for i = 0, @bufferSize - 1
x = i / (@bufferSize - 1) * lg.getWidth!
y = (-buf[i] * 0.25 / 2 + 0.5) * lg.getHeight!
ltable.push line, x, y
lg.setColor 1, 1, 1, 0.05
lg.setLineWidth 5
lg.line line
lg.pop!
-- Draw grid
do
p1 = vec2 @cam\toWorld 0, 0
p2 = vec2 @cam\toWorld lg.getWidth!, 0
p3 = vec2 @cam\toWorld lg.getWidth!, lg.getHeight!
p4 = vec2 @cam\toWorld 0, lg.getHeight!
tl = p1\min p2, p3, p4
br = p1\max p2, p3, p4
tl = vec2 math.floor(tl.x / @gridSize.x), math.floor(tl.y / @gridSize.y)
br = vec2 math.ceil(br.x / @gridSize.x), math.ceil(br.y / @gridSize.y)
lg.setColor 1, 1, 1, @snapping and 0.05 or 0.02
for y = tl.y, br.y
for x = tl.x, br.x
pos = @gridSize * vec2 x, y
lg.circle 'fill', pos.x, pos.y, 4, 16
-- Draw module bodies
for module in *@modules
lg.push 'all'
lg.translate module.pos.x, module.pos.y
module\draw!
lg.pop!
-- Draw module connections
lg.setLineWidth 4
for module in *@modules
for connection in *module.inputConnections
p1 = @getModuleOutputPosition connection[1], connection[2]
p2 = @getModuleInputPosition module, connection[3]
lg.setColor 0.4, 0.35, 0.7, 1
if @rmode.kind == 'cut' and @checkConnectionIntersects p1, p2, @getCutLine!
lg.setColor 0.9, 0.1, 0.1, 1
@drawConnection p1, p2
-- Draw dragged connection
if @mode.kind == 'connect'
socket = @mode.from
output = @getModuleOutputPosition socket[1], socket[2]
if output\dist(wpos) > 16
input = wpos
if @hoveredSocket and @hoveredSocket[3]
input = @getModuleInputPosition @hoveredSocket[1], @hoveredSocket[2]
lg.setColor 0.4, 0.35, 0.7, 1
@drawConnection output, input
-- Draw module sockets
for module in *@modules
lg.push 'all'
lg.translate module.pos.x, module.pos.y
module\drawSockets!
lg.pop!
-- Draw cut
if @rmode.kind == 'cut'
lg.push 'all'
start, stop = @getCutLine!
lg.setLineWidth 4 / @getZoomAmount!
lg.setColor 0.9, 0.1, 0.1, 1
lg.line start.x, start.y, stop.x, stop.y
lg.pop!
-- Draw selection
if @mode.kind == 'select'
lg.push 'all'
-- lg.origin!
tl, br = @getSelection!
radius = 16
pad = vec2 radius, radius
tl -= pad
br += pad
size = br - tl
lg.setColor 0.2, 0.2, 0.7, 0.4
lg.rectangle 'fill', tl.x, tl.y, size.x, size.y, radius, radius, 32
lg.setBlendMode 'add'
lg.setColor 0.5, 0.5, 0.6, 0.1
lg.rectangle 'fill', tl.x, tl.y, size.x, size.y, radius, radius, 32
lg.setBlendMode 'alpha'
lg.setColor 0.4, 0.4, 0.7, 1
lg.setLineWidth 3 / @getZoomAmount!
lg.rectangle 'line', tl.x, tl.y, size.x, size.y, radius, radius, 32
lg.pop!
-- Draw hovered socket label
if @hoveredSocket
lg.push 'all'
module = @hoveredSocket[1]
font = module.font
lg.setFont font
label = if @hoveredSocket[3]
module.inputLabels[@hoveredSocket[2]]
else
module.outputLabels[@hoveredSocket[2]]
label = label or (@hoveredSocket[3] and 'input' or 'output')
point = if @hoveredSocket[3]
module\getInputPosition @hoveredSocket[2]
else
module\getOutputPosition @hoveredSocket[2]
point += module.pos
size = vec2 font\getWidth(label), font\getHeight!
xoffset = if @hoveredSocket[3]
-40 - size.x
else
40
point += vec2 xoffset, -size.y / 2
pad = vec2 8, 8
tl = point - pad
br = tl + size + pad * 2
boxSize = br - tl
lg.setColor 0.05, 0.05, 0.05, 0.4
lg.rectangle 'fill', tl.x, tl.y, boxSize.x, boxSize.y, 8, nil, 16
lg.setColor 0.5, 0.5, 0.55, 1
lg.print label, lmath.round(point.x), lmath.round(point.y)
lg.pop!
-- Draw module selection panel and hint overlay
do
lg.push 'all'
lg.origin!
-- Draw hints
if option.hint_overlay
font = @infoFont
font\setLineHeight 1.5
lg.setFont font
inner = aabb2.fromLove!
inner\padLeft -@panelWidth if @panelOpen
inner\pad -24
lg.setColor 1, 1, 1, 0.1
text = 'Press TAB to toggle module panel\nPress ` to toggle grid snapping'
inner\drawText text, vec2!, true
inner\drawText 'Press F11 to toggle fullscreen', vec2(1, 0), true
if @exportStatus
inner\drawText @exportStatus, vec2(1, 1), true
inner\padBottom(-font\getHeight! * font\getLineHeight!)\drawText 'Press F1 to open export folder', vec2(1, 1), true
-- Draw panel
if @panelOpen
font = @panelFont
lg.setFont font
lg.setColor 0.2, 0.2, 0.22, 0.5
lg.rectangle 'fill', 0, 0, @panelWidth, lg.getHeight!
mpos = vec2.fromLoveMouse!
for i, name in ipairs @moduleNames
bbox = @getPanelButtonBBox i
-- FIXME: Kind of a hack, we should check if the button is active instead of checking for mouse button
shouldHighlight = not love.mouse.isDown(1) and bbox\hasPoint mpos
-- Draw button background
lg.setColor 0.17, 0.17, 0.19, 1
util.highlight! if shouldHighlight
bbox\drawRectangle 8
-- Draw button number
if i <= 10
i %= 10
lg.setColor 0.4, 0.4, 0.45, 0.2
util.highlight! if shouldHighlight
bbox\pad(-16)\drawText i, vec2(0, 0.5), true
-- Draw button label
lg.setColor 0.4, 0.4, 0.45, 1
util.highlight! if shouldHighlight
label = require('module.' .. name).name
bbox\drawText label, nil, true
lg.pop!
lg.pop!
@cam\detach!
getPanelButtonBBox: (index) =>
assert index <= #@moduleNames
pad = 16
spacing = 24
height = 48
y = pad + (index - 1) * (height + spacing) - @panelOffset
return aabb2 pad, @panelWidth - pad, y, y + height
getConnectionBezier: (fromOutputPoint, toInputPoint) =>
c1x = lmath.lerp 1 / 2, fromOutputPoint.x, toInputPoint.x
c2x = lmath.lerp 1 / 2, fromOutputPoint.x, toInputPoint.x
c1x = math.max c1x, fromOutputPoint.x + 96
c2x = math.min c2x, toInputPoint.x - 96
c1 = vec2 c1x, fromOutputPoint.y
c2 = vec2 c2x, toInputPoint.y
return love.math.newBezierCurve fromOutputPoint.x, fromOutputPoint.y, c1.x, c1.y, c2.x, c2.y, toInputPoint.x, toInputPoint.y
checkConnectionIntersects: (fromOutputPoint, toInputPoint, p1, p2) =>
curve = @getConnectionBezier fromOutputPoint, toInputPoint
line = curve\render 2
ppoint, cpoint = nil, vec2 line[1], line[2]
for i = 3, #line, 2
ppoint = cpoint
cpoint = vec2 line[i], line[i + 1]
return true if util.checkLinesIntersect ppoint, cpoint, p1, p2
return false
drawConnection: (fromOutputPoint, toInputPoint) =>
curve = @getConnectionBezier fromOutputPoint, toInputPoint
line = curve\render 4
love.graphics.line line
getZoomAmount: => 2 ^ @zoom
keypressed: (key) =>
if key == 'delete'
@deleteSelected!
return
if key == 'f1'
assert love.system.openURL love.filesystem.getSaveDirectory!
return
if key == 'tab'
@panelOpen = not @panelOpen
return
if key == '`'
@snapping = not @snapping
return
num = tonumber key
if num
num = 10 if num == 0
name = @moduleNames[num]
if name
@spawnModule name, @getMousePos!
keyreleased: =>
mousepressed: (x, y, button) =>
if @panelOpen and x < @panelWidth
mpos = vec2 x, y
for i, name in ipairs @moduleNames
bbox = @getPanelButtonBBox i
if bbox\hasPoint mpos
@spawnModule name
break
return
wpos = @getMousePos!
if button == 1
socket = @getSocketAtPos wpos
if socket
if socket[3] -- Try disconnect from input
for i, connection in ltable.ripairs socket[1].inputConnections
if connection[3] == socket[2]
table.remove socket[1].inputConnections, i
@mode = {
kind: 'connect'
from: {connection[1], connection[2], false}
}
break
return
@mode = {
kind: 'connect'
from: socket
}
return
module = @getModuleAtPos wpos
if module
unless module.selected
@clearSelected! unless love.keyboard.isDown 'lshift', 'rshift'
module.selected = true
if wpos.y < module.pos.y + module.labelHeight
-- Raise the module up in z order
for i, mod in ipairs @modules
if mod == module
table.insert @modules, table.remove @modules, i
break
-- Only select dragged module when it's not already selected
selected = [mod for mod in *@modules when mod.selected]
positions = [mod.pos.copy for mod in *selected]
@mode = {
kind: 'position'
fromPoint: wpos.copy
fromPoss: positions
modules: selected
}
return
mpos = module.pos
module\mousepressed wpos.x - mpos.x, wpos.y - mpos.y - module.labelHeight, button
@activeModule = module
return
-- Do selection
@clearSelected!
@mode = {
kind: 'select'
fromPoint: wpos.copy
}
if button == 2
module = @getModuleAtPos wpos
if module
unless wpos.y < module.pos.y + module.labelHeight
mpos = module.pos
module\mousepressed wpos.x - mpos.x, wpos.y - mpos.y - module.labelHeight, button
@ractiveModule = module
return
@rmode = {
kind: 'cut'
fromPoint: wpos.copy
}
if button == 3
@dragInfo = {
fromPoint: wpos.copy
}
mousereleased: (x, y, button) =>
wpos = @getMousePos!
if button == 1
if @activeModule
mpos = @activeModule.pos
@activeModule\mousereleased wpos.x - mpos.x, wpos.y - mpos.y - @activeModule.labelHeight, button
@activeModule = nil
return
if @mode.kind == 'connect'
to = @getSocketAtPos @getMousePos!
if to and to[3] -- Is input socket
output, outputIndex = @mode.from[1], @mode.from[2]
input, inputIndex = to[1], to[2]
canConnect = true
for connection in *input.inputConnections
if connection[1] == output and connection[2] == outputIndex and connection[3] == inputIndex
canConnect = false
break
input\connect output, outputIndex, inputIndex if canConnect
@mode = { kind: 'none' }
return
if button == 2
if @ractiveModule
mpos = @ractiveModule.pos
@ractiveModule\mousereleased wpos.x - mpos.x, wpos.y - mpos.y - @ractiveModule.labelHeight, button
@ractiveModule = nil
return
if @rmode.kind == 'cut'
for module in *@modules
for i, connection in ltable.ripairs module.inputConnections
p1 = @getModuleOutputPosition connection[1], connection[2]
p2 = @getModuleInputPosition module, connection[3]
if @checkConnectionIntersects p1, p2, @getCutLine!
table.remove module.inputConnections, i
@rmode = { kind: 'none' }
return
if button == 3
@dragInfo = nil
return
wheelmoved: (dx, dy) =>
if @panelOpen and love.mouse.getX! < @panelWidth
@scrollPanel dy
return
wpos = @getMousePos!
@zoom += dy * 0.25
@zoom = lmath.clamp @zoom, -3, 2
@cam\setZoom @getZoomAmount!
newWPos = vec2 @cam\toWorld love.mouse.getPosition!
delta = wpos\delta newWPos
@cam\setPos (-delta + vec2 @cam\getPos!)\split!
resize: (width, height) =>
@cam\setDirty!
filedropped: (file) =>
status, sdata = pcall love.sound.newSoundData, file
if status
mod = @spawnModule 'recorder', @getMousePos!
ccount = sdata\getChannelCount!
scount = sdata\getSampleCount!
i = 0
while true
t = i / @sampleRate
index = t * sdata\getSampleRate!
index0 = math.floor index
index1 = math.ceil index
fract = index % 1
break if index1 >= scount
bufIndex = 1 + math.floor i / @bufferSize
buf = mod.display.buffers[bufIndex] or buffer.new @bufferSize
mod.display.buffers[bufIndex] = buf
sample = 0
for c = 1, ccount
sample += lmath.lerp fract, sdata\getSample(index0, c), sdata\getSample(index1, c)
buf[i % @bufferSize] = sample / ccount
i += 1
getModuleInputPosition: (module, index) => module.pos + module\getInputPosition index
getModuleOutputPosition: (module, index) => module.pos + module\getOutputPosition index
getCutLine: =>
return nil if @rmode.kind != 'cut'
return @rmode.fromPoint, @getMousePos!
getMousePos: => vec2 @cam\toWorld love.mouse.getPosition!
clearSelected: =>
for module in *@modules
module.selected = nil
deleteSelected: =>
deleted = {}
for i, module in ltable.ripairs @modules
table.insert deleted, table.remove @modules, i if module.selected
for module in *deleted
for other in *@modules
for i, connection in ltable.ripairs other.inputConnections
table.remove other.inputConnections, i if connection[1] == module
getSelection: =>
return nil unless @mode.kind == 'select'
start = @mode.fromPoint
wpos = @getMousePos!
tl = start\min wpos
br = start\max wpos
pad = vec2 4, 4
return tl - pad, br + pad
isModuleInSelection: (module) =>
tl, br = @getSelection!
return false unless tl
mtl = module.pos
mbr = mtl + module.size
return tl.x <= mtl.x and tl.y <= mtl.y and br.x >= mbr.x and br.y >= mbr.y
getModuleAtPos: (pos) =>
return nil if @hoveredSocket
for _, module in ltable.ripairs @modules
tl = module.pos
br = tl + module.size
if pos.x >= tl.x and pos.y >= tl.y and pos.x < br.x and pos.y < br.y
return module
return nil
getSocketAtPos: (point) =>
minDist = math.huge
local minSocket
for module in *@modules
for i = 1, module\getInputCount!
pos = @getModuleInputPosition module, i
dist = pos\dist point
minDist, minSocket = dist, {module, i, true} if dist < minDist
for i = 1, module\getOutputCount!
pos = @getModuleOutputPosition module, i
dist = pos\dist point
minDist, minSocket = dist, {module, i, false} if dist < minDist
return minSocket if minDist <= 24
return nil
scrollPanel: (dy) =>
lastBBox = @getPanelButtonBBox #@moduleNames
limit = lastBBox.y1 + @panelOffset - 8
@panelOffset -= dy * 32
@panelOffset = lmath.clamp @panelOffset, 0, limit
spawnModule: (name, point) =>
unless point
x, y = lg.getDimensions!
point = vec2 @cam\toWorld x / 2, y / 2
Module = require 'module.' .. name
mod = Module @, point
mod.pos -= mod.size / 2
@addModule mod
mod\snapIfNeeded!
return mod
addModule: (mod) =>
-- NOTE: We're setting the workspace in the constructor, that's kinda sus but whatever
-- mod.workspace = @
table.insert @modules, mod
removeModule: (mod) =>
assert mod.workspace == @, "trying to remove a module that doesn't belong to the workspace"
for i = 1, #@modules
if @modules[i] == mod
mod.workspace = nil
table.remove @modules, i
return
error "could not find module in workspace"