Browse Source

Initial commit

tags/v0.3.0
Andrew Belt 8 years ago
commit
c1cae82918
45 changed files with 3352 additions and 0 deletions
  1. +165
    -0
      LICENSE.txt
  2. +78
    -0
      Makefile
  3. +15
    -0
      README.txt
  4. +179
    -0
      src/5V.hpp
  5. +203
    -0
      src/core/AudioInterface.cpp
  6. +207
    -0
      src/core/MidiInterface.cpp
  7. +9
    -0
      src/core/core.cpp
  8. +18
    -0
      src/core/core.hpp
  9. +294
    -0
      src/gui.cpp
  10. +4
    -0
      src/gui.cpp.d
  11. +32
    -0
      src/main.cpp
  12. +98
    -0
      src/plugin.cpp
  13. +3
    -0
      src/plugin.cpp.d
  14. +180
    -0
      src/rack.cpp
  15. +1
    -0
      src/rack.cpp.d
  16. +34
    -0
      src/rack.hpp
  17. +117
    -0
      src/util.hpp
  18. +398
    -0
      src/widgets.hpp
  19. +24
    -0
      src/widgets/Button.cpp
  20. +6
    -0
      src/widgets/ChoiceButton.cpp
  21. +34
    -0
      src/widgets/InputPort.cpp
  22. +21
    -0
      src/widgets/Knob.cpp
  23. +6
    -0
      src/widgets/Label.cpp
  24. +35
    -0
      src/widgets/Light.cpp
  25. +27
    -0
      src/widgets/Menu.cpp
  26. +6
    -0
      src/widgets/MenuEntry.cpp
  27. +26
    -0
      src/widgets/MenuItem.cpp
  28. +6
    -0
      src/widgets/MenuLabel.cpp
  29. +10
    -0
      src/widgets/MenuOverlay.cpp
  30. +163
    -0
      src/widgets/ModuleWidget.cpp
  31. +34
    -0
      src/widgets/OutputPort.cpp
  32. +29
    -0
      src/widgets/ParamWidget.cpp
  33. +54
    -0
      src/widgets/Port.cpp
  34. +17
    -0
      src/widgets/QuantityWidget.cpp
  35. +264
    -0
      src/widgets/RackWidget.cpp
  36. +22
    -0
      src/widgets/Scene.cpp
  37. +10
    -0
      src/widgets/Screw.cpp
  38. +38
    -0
      src/widgets/ScrollBar.cpp
  39. +42
    -0
      src/widgets/ScrollWidget.cpp
  40. +29
    -0
      src/widgets/Slider.cpp
  41. +26
    -0
      src/widgets/SpriteWidget.cpp
  42. +171
    -0
      src/widgets/Toolbar.cpp
  43. +16
    -0
      src/widgets/Tooltip.cpp
  44. +89
    -0
      src/widgets/Widget.cpp
  45. +112
    -0
      src/widgets/WireWidget.cpp

+ 165
- 0
LICENSE.txt View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.


This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.

0. Additional Definitions.

As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.

"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.

An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.

A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".

The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.

The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.

1. Exception to Section 3 of the GNU GPL.

You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.

2. Conveying Modified Versions.

If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:

a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or

b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.

3. Object Code Incorporating Material from Library Header Files.

The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:

a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.

b) Accompany the object code with a copy of the GNU GPL and this license
document.

4. Combined Works.

You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:

a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.

b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.

c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.

d) Do one of the following:

0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.

1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.

e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)

5. Combined Libraries.

You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:

a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.

b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.

6. Revised Versions of the GNU Lesser General Public License.

The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.

Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.

If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

+ 78
- 0
Makefile View File

@@ -0,0 +1,78 @@
ARCH ?= linux
CFLAGS = -MMD -g -Wall -O0 \
-DNOC_FILE_DIALOG_IMPLEMENTATION
CXXFLAGS = -MMD -g -Wall -std=c++11 -O0 -ffast-math \
-I./lib -I./src
LDFLAGS =

SOURCES = $(wildcard src/*.cpp src/*/*.cpp) \
lib/nanovg/src/nanovg.c


# Linux
ifeq ($(ARCH), linux)
CC = gcc
CXX = g++
SOURCES += lib/noc/noc_file_dialog.c
CFLAGS += -DNOC_FILE_DIALOG_GTK $(shell pkg-config --cflags gtk+-2.0)
CXXFLAGS += -DLINUX
LDFLAGS = -lpthread -lGL -lGLEW -lglfw -ldl -lrtaudio -lrtmidi -lprofiler -ljansson \
$(shell pkg-config --libs gtk+-2.0)
TARGET = 5V
endif

# Apple
ifeq ($(ARCH), apple)
OSXCROSS = $(HOME)/pkg/osxcross/target/bin
CC = $(OSXCROSS)/x86_64-apple-darwin15-cc
CXX = $(OSXCROSS)/x86_64-apple-darwin15-c++
SOURCES += lib/noc/noc_file_dialog.m
CFLAGS += -DNOC_FILE_DIALOG_OSX
CXXFLAGS += -DAPPLE -stdlib=libc++
TARGET = 5V
endif

# Windows
ifeq ($(ARCH), windows)
CC = x86_64-w64-mingw32-gcc
CXX = x86_64-w64-mingw32-g++
SOURCES += lib/noc/noc_file_dialog.c
CFLAGS += -DNOC_FILE_DIALOG_WIN32
CXXFLAGS += -DWINDOWS
LDFLAGS = -lpthread -ljansson \
./lib/nanovg/build/libnanovg.a -lglfw3 -lgdi32 -lopengl32 -lglew32 \
./lib/rtaudio/librtaudio.a -lksuser -luuid \
./lib/rtmidi/librtmidi.a -lwinmm \
-lcomdlg32 -lole32 \
-Wl,--export-all-symbols,--out-implib,lib5V.a -mwindows
TARGET = 5V.exe
endif


OBJECTS = $(patsubst %, build/%.o, $(SOURCES))
DEPS = $(patsubst %, build/%.d, $(SOURCES))


# Final targets

all: $(TARGET)

$(TARGET): $(OBJECTS)
$(CXX) -o $@ $^ $(LDFLAGS)

# Object targets

-include $(DEPS)

build/%.c.o: %.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c -o $@ $<

build/%.cpp.o: %.cpp
@mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -c -o $@ $<

# Utilities

clean:
rm -rf build

+ 15
- 0
README.txt View File

@@ -0,0 +1,15 @@
░█▀▄░█▀█░█▀▀░█░█
░█▀▄░█▀█░█░░░█▀▄
░▀░▀░▀░▀░▀▀▀░▀░▀

Virtual modular synthesizer engine

## Building

The build system is a mess, but I'll add instructions before I announce the software.

## License

All source code in this repository is licensed under the LGPLv3.

LGPL was chosen over GPL to allow plugins to call functions and inherit object classes from this source code without being forced to use the GPL license. You are thus free to develop proprietary plugins which link to and use the API of this software.

+ 179
- 0
src/5V.hpp View File

@@ -0,0 +1,179 @@
#pragma once

#include <string>
#include <list>
#include <memory>
#include "widgets.hpp"
#include "rack.hpp"


extern Scene *gScene;
extern RackWidget *gRackWidget;

////////////////////
// Plugin manager
////////////////////

struct Model;

// Subclass this and return a pointer to a new one when init() is called
struct Plugin {
virtual ~Plugin();

// A unique identifier for your plugin, e.g. "simple"
std::string slug;
// Human readable name for your plugin, e.g. "Simple Modular"
std::string name;
// A list of the models made available by this plugin
std::list<Model*> models;
};

struct Model {
virtual ~Model() {}

Plugin *plugin;
// A unique identifier for the model in this plugin, e.g. "vco"
std::string slug;
// Human readable name for your model, e.g. "VCO"
std::string name;
virtual ModuleWidget *createModuleWidget() { return NULL; }
};

extern std::list<Plugin*> gPlugins;

void pluginInit();
void pluginDestroy();

////////////////////
// midi.cpp
////////////////////

void midiInit();
void midiDestroy();
int midiPortCount();
std::string midiPortName(int portId);
void midiPortOpen(int portId);
void midiPortClose();

////////////////////
// gui.cpp
////////////////////

extern Vec gMousePos;
extern Widget *gHoveredWidget;
extern Widget *gDraggedWidget;

void guiInit();
void guiDestroy();
void guiRun();
void guiCursorLock();
void guiCursorUnlock();

int loadFont(std::string filename);
int loadImage(std::string filename);
void drawImage(NVGcontext *vg, Vec pos, int imageId);

////////////////////
// rack.cpp
////////////////////

void rackInit();
void rackDestroy();
void rackStart();
void rackStop();
// Does not transfer ownership
void rackAddModule(Module *module);
void rackRemoveModule(Module *module);
// Does not transfer ownership
void rackConnectWire(Wire *wire);
void rackDisconnectWire(Wire *wire);
long rackGetFrame();
void rackRequestFrame(long frame);
void rackSetParamSmooth(Module *module, int paramId, float value);

////////////////////
// Implemented by plugin
////////////////////

// Called once to initialize and return Plugin.
// Plugin is destructed by the 5V engine when it closes
extern "C"
Plugin *init();

////////////////////
// Optional helpers for plugins
////////////////////

inline
Plugin *createPlugin(std::string slug, std::string name) {
Plugin *plugin = new Plugin();
plugin->slug = slug;
plugin->name = name;
return plugin;
}

template <class TModuleWidget>
Model *createModel(Plugin *plugin, std::string slug, std::string name) {
struct TModel : Model {
ModuleWidget *createModuleWidget() {
ModuleWidget *moduleWidget = new TModuleWidget();
moduleWidget->model = this;
return moduleWidget;
}
};
Model *model = new TModel();
model->plugin = plugin;
model->slug = slug;
model->name = name;
// Create bi-directional association between the Plugin and Model
if (plugin) {
plugin->models.push_back(model);
}
return model;
}

template <class TParam>
ParamWidget *createParamWidget(ModuleWidget *moduleWidget, int paramId, float minValue, float maxValue, float defaultValue, Vec pos) {
ParamWidget *param = new TParam();
param->moduleWidget = moduleWidget;
param->paramId = paramId;
param->setLimits(minValue, maxValue);
param->setDefaultValue(defaultValue);
param->box.pos = pos;
// Create bi-directional association between the Param and ModelWidget
moduleWidget->params[paramId] = param;
moduleWidget->addChild(param);
return param;
}

inline
InputPort *createInputPort(ModuleWidget *moduleWidget, int inputId, Vec pos) {
InputPort *port = new InputPort();
port->moduleWidget = moduleWidget;
port->inputId = inputId;
port->box.pos = pos;
// Create bi-directional association between the InputPort and ModelWidget
moduleWidget->inputs[inputId] = port;
moduleWidget->addChild(port);
return port;
}

inline
OutputPort *createOutputPort(ModuleWidget *moduleWidget, int outputId, Vec pos) {
OutputPort *port = new OutputPort();
port->moduleWidget = moduleWidget;
port->outputId = outputId;
port->box.pos = pos;
// Create bi-directional association between the OutputPort and ModelWidget
moduleWidget->outputs[outputId] = port;
moduleWidget->addChild(port);
return port;
}

inline
Screw *createScrew(ModuleWidget *moduleWidget, Vec pos) {
Screw *screw = new Screw();
screw->box.pos = pos;
moduleWidget->addChild(screw);
return screw;
}

+ 203
- 0
src/core/AudioInterface.cpp View File

@@ -0,0 +1,203 @@
#include "core.hpp"
#include <assert.h>
#include <mutex>
#include <condition_variable>
#include <rtaudio/RtAudio.h>


#define AUDIO_BUFFER_SIZE 16384

struct AudioInterface : Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
AUDIO1_INPUT,
AUDIO2_INPUT,
NUM_INPUTS
};
enum OutputIds {
NUM_OUTPUTS
};

float audio1Buffer[AUDIO_BUFFER_SIZE] = {};
float audio2Buffer[AUDIO_BUFFER_SIZE] = {};
// Current frame for step(), called by the rack thread
long bufferFrame = 0;
// Current frame for processAudio(), called by audio thread
long audioFrame = 0;
long audioFrameNeeded = -1;
RtAudio audio;
// The audio thread should wait on the rack thread until the buffer has enough samples
std::mutex mutex;
std::condition_variable cv;
bool running;

AudioInterface();
~AudioInterface();
void step();

void openDevice(int deviceId);
void closeDevice();
// Blocks until the buffer has enough samples
void processAudio(float *outputBuffer, int frameCount);
};


AudioInterface::AudioInterface() {
params.resize(NUM_PARAMS);
inputs.resize(NUM_INPUTS);
outputs.resize(NUM_OUTPUTS);
}

AudioInterface::~AudioInterface() {
closeDevice();
}

void AudioInterface::step() {
int i = bufferFrame % AUDIO_BUFFER_SIZE;
audio1Buffer[i] = getf(inputs[AUDIO1_INPUT]);
audio2Buffer[i] = getf(inputs[AUDIO2_INPUT]);
std::unique_lock<std::mutex> lock(mutex);
bufferFrame++;
if (bufferFrame == audioFrameNeeded) {
lock.unlock();
cv.notify_all();
}
}

int audioCallback(void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, void *userData) {
AudioInterface *that = (AudioInterface*) userData;
that->processAudio((float*) outputBuffer, nBufferFrames);
return 0;
}

void AudioInterface::openDevice(int deviceId) {
assert(!audio.isStreamOpen());
if (deviceId < 0) {
deviceId = audio.getDefaultOutputDevice();
}

RtAudio::StreamParameters streamParams;
streamParams.deviceId = deviceId;
streamParams.nChannels = 2;
streamParams.firstChannel = 0;
unsigned int sampleRate = SAMPLE_RATE;
unsigned int bufferFrames = 256;

audioFrame = -1;
running = true;

try {
audio.openStream(&streamParams, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &audioCallback, this);
audio.startStream();
}
catch (RtAudioError &e) {
printf("Could not open audio stream: %s\n", e.what());
}
}

void AudioInterface::closeDevice() {
if (!audio.isStreamOpen()) {
return;
}
{
std::unique_lock<std::mutex> lock(mutex);
running = false;
}
cv.notify_all();

try {
// Blocks until stream thread exits
audio.stopStream();
audio.closeStream();
}
catch (RtAudioError &e) {
printf("Could not close audio stream: %s\n", e.what());
}
}

void AudioInterface::processAudio(float *outputBuffer, int frameCount) {
std::unique_lock<std::mutex> lock(mutex);
if (audioFrame < 0) {
// This audio thread is new. Reset the frame positions
audioFrame = rackGetFrame();
bufferFrame = audioFrame;
}
audioFrameNeeded = audioFrame + frameCount;
rackRequestFrame(audioFrameNeeded);
// Wait for needed frames
while (running && bufferFrame < audioFrameNeeded) {
cv.wait(lock);
}
// Copy values from internal buffer to audio buffer, while holding the mutex just in case our audio buffer wraps around
for (int frame = 0; frame < frameCount; frame++) {
int i = audioFrame % AUDIO_BUFFER_SIZE;
outputBuffer[2*frame + 0] = audio1Buffer[i] / 5.0;
outputBuffer[2*frame + 1] = audio2Buffer[i] / 5.0;
audioFrame++;
}
}


struct AudioItem : MenuItem {
AudioInterface *audioInterface;
int deviceId;
void onAction() {
audioInterface->closeDevice();
audioInterface->openDevice(deviceId);
}
};

struct AudioChoice : ChoiceButton {
AudioInterface *audioInterface;
void onAction() {
MenuOverlay *overlay = new MenuOverlay();
Menu *menu = new Menu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));

int deviceCount = audioInterface->audio.getDeviceCount();
if (deviceCount == 0) {
MenuLabel *label = new MenuLabel();
label->text = "No audio devices";
menu->pushChild(label);
}
for (int deviceId = 0; deviceId < deviceCount; deviceId++) {
RtAudio::DeviceInfo info = audioInterface->audio.getDeviceInfo(deviceId);
if (!info.probed)
continue;

char text[100];
snprintf(text, 100, "%s (%d in, %d out)", info.name.c_str(), info.inputChannels, info.outputChannels);

AudioItem *audioItem = new AudioItem();
audioItem->audioInterface = audioInterface;
audioItem->deviceId = deviceId;
audioItem->text = text;
menu->pushChild(audioItem);
}
overlay->addChild(menu);
gScene->addChild(overlay);
}
};


AudioInterfaceWidget::AudioInterfaceWidget() : ModuleWidget(new AudioInterface()) {
box.size = Vec(15*4, 380);
inputs.resize(AudioInterface::NUM_INPUTS);

createInputPort(this, AudioInterface::AUDIO1_INPUT, Vec(15, 120));
createInputPort(this, AudioInterface::AUDIO2_INPUT, Vec(15, 170));

AudioChoice *audioChoice = new AudioChoice();
audioChoice->audioInterface = dynamic_cast<AudioInterface*>(module);
audioChoice->text = "Audio Interface";
audioChoice->box.pos = Vec(0, 0);
audioChoice->box.size.x = box.size.x;
addChild(audioChoice);
}

void AudioInterfaceWidget::draw(NVGcontext *vg) {
bndBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);
ModuleWidget::draw(vg);
}

+ 207
- 0
src/core/MidiInterface.cpp View File

@@ -0,0 +1,207 @@
#include "core.hpp"
#include <assert.h>
#include <rtmidi/RtMidi.h>
#include <list>
#include <algorithm>


struct MidiInterface : Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
NUM_INPUTS
};
enum OutputIds {
GATE_OUTPUT,
PITCH_OUTPUT,
NUM_OUTPUTS
};

RtMidiIn midi;
std::list<int> notes;
bool pedal = false;
bool gate = false;
int note = 64; // C4
int pitchWheel = 64;

MidiInterface();
~MidiInterface();
void step();

void openPort(int portId);
void closePort();
void pressNote(int note);
void releaseNote(int note);
void processMidi(long msg);
};


void midiCallback(double timeStamp, std::vector<unsigned char> *message, void *userData) {
MidiInterface *that = (MidiInterface*) userData;
if (message->size() < 3)
return;

long msg = (message->at(0)) | (message->at(1) << 8) | (message->at(2) << 16);
that->processMidi(msg);
}

MidiInterface::MidiInterface() {
params.resize(NUM_PARAMS);
inputs.resize(NUM_INPUTS);
outputs.resize(NUM_OUTPUTS);
midi.setCallback(midiCallback, this);
}

MidiInterface::~MidiInterface() {
closePort();
}

void MidiInterface::step() {
if (outputs[GATE_OUTPUT]) {
*outputs[GATE_OUTPUT] = gate ? 5.0 : 0.0;
}
if (outputs[PITCH_OUTPUT]) {
*outputs[PITCH_OUTPUT] = ((note - 64) + 2.0*(pitchWheel - 64) / 64.0) / 12.0;
}
}

void MidiInterface::openPort(int portId) {
closePort();
try {
midi.openPort(portId);
}
catch (RtMidiError &e) {
printf("Could not open midi port: %s\n", e.what());
}
}

void MidiInterface::closePort() {
if (!midi.isPortOpen())
return;
midi.closePort();
}

void MidiInterface::pressNote(int note) {
auto it = std::find(notes.begin(), notes.end(), note);
if (it != notes.end())
notes.erase(it);
notes.push_back(note);
this->gate = true;
this->note = note;
}

void MidiInterface::releaseNote(int note) {
// Remove the note
auto it = std::find(notes.begin(), notes.end(), note);
if (it != notes.end())
notes.erase(it);

if (pedal) {
// Don't release if pedal is held
}
else if (!notes.empty()) {
// Play previous note
auto it2 = notes.end();
it2--;
this->note = *it2;
}
else {
// No notes are held, turn the gate off
this->gate = false;
}
}

void MidiInterface::processMidi(long msg) {
int channel = msg & 0xf;
int status = (msg >> 4) & 0xf;
int data1 = (msg >> 8) & 0xff;
int data2 = (msg >> 16) & 0xff;

if (channel != 0)
return;
printf("channel %d status %d data1 %d data2 %d\n", channel, status, data1, data2);

switch (status) {
// note off
case 0x8: {
releaseNote(data1);
} break;
case 0x9: // note on
if (data2) {
pressNote(data1);
}
else {
// For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released.
releaseNote(data1);
}
break;
case 0xb: // cc
switch (data1) {
case 0x40:
pedal = (data2 >= 64);
releaseNote(-1);
break;
}
break;
case 0xe: // pitch wheel
this->pitchWheel = data2;
break;
}
}


struct MidiItem : MenuItem {
MidiInterface *midiInterface;
int portId;
void onAction() {
midiInterface->closePort();
midiInterface->openPort(portId);
}
};

struct MidiChoice : ChoiceButton {
MidiInterface *midiInterface;
void onAction() {
MenuOverlay *overlay = new MenuOverlay();
Menu *menu = new Menu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));

int portCount = midiInterface->midi.getPortCount();
if (portCount == 0) {
MenuLabel *label = new MenuLabel();
label->text = "No MIDI devices";
menu->pushChild(label);
}
for (int portId = 0; portId < portCount; portId++) {
MidiItem *midiItem = new MidiItem();
midiItem->midiInterface = midiInterface;
midiItem->portId = portId;
midiItem->text = midiInterface->midi.getPortName();
menu->pushChild(midiItem);
}
overlay->addChild(menu);
gScene->addChild(overlay);
}
};


MidiInterfaceWidget::MidiInterfaceWidget() : ModuleWidget(new MidiInterface()) {
box.size = Vec(15*4, 380);
outputs.resize(MidiInterface::NUM_OUTPUTS);

createOutputPort(this, MidiInterface::GATE_OUTPUT, Vec(15, 120));
createOutputPort(this, MidiInterface::PITCH_OUTPUT, Vec(15, 170));

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiInterface = dynamic_cast<MidiInterface*>(module);
midiChoice->text = "MIDI Interface";
midiChoice->box.pos = Vec(0, 0);
midiChoice->box.size.x = box.size.x;
addChild(midiChoice);
}

void MidiInterfaceWidget::draw(NVGcontext *vg) {
bndBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);
ModuleWidget::draw(vg);
}

+ 9
- 0
src/core/core.cpp View File

@@ -0,0 +1,9 @@
#include "core.hpp"


Plugin *coreInit() {
Plugin *plugin = createPlugin("Core", "Core");
createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface");
createModel<MidiInterfaceWidget>(plugin, "MidiInterface", "MIDI Interface");
return plugin;
}

+ 18
- 0
src/core/core.hpp View File

@@ -0,0 +1,18 @@
#include "../5V.hpp"


Plugin *coreInit();

////////////////////
// module widgets
////////////////////

struct AudioInterfaceWidget : ModuleWidget {
AudioInterfaceWidget();
void draw(NVGcontext *vg);
};

struct MidiInterfaceWidget : ModuleWidget {
MidiInterfaceWidget();
void draw(NVGcontext *vg);
};

+ 294
- 0
src/gui.cpp View File

@@ -0,0 +1,294 @@
#include "5V.hpp"
#include <unistd.h>

// #define NANOVG_GLEW
#define NANOVG_IMPLEMENTATION
#include "../lib/nanovg/src/nanovg.h"
#define NANOVG_GL2_IMPLEMENTATION
#include "../lib/nanovg/src/nanovg_gl.h"
#define BLENDISH_IMPLEMENTATION
#include "../lib/oui/blendish.h"


static GLFWwindow *window;
static NVGcontext *vg = NULL;

Vec gMousePos;
Widget *gHoveredWidget = NULL;
Widget *gDraggedWidget = NULL;


void windowSizeCallback(GLFWwindow* window, int width, int height) {
gScene->box.size = Vec(width, height);
gScene->onResize();
}

void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods) {
if (gHoveredWidget) {
// onMouseDown and onMouseUp
if (action == GLFW_PRESS) {
gHoveredWidget->onMouseDown(button);
}
else if (action == GLFW_RELEASE) {
gHoveredWidget->onMouseUp(button);
}
}

// onDragStart, onDragEnd, and onDragDrop
if (button == GLFW_MOUSE_BUTTON_LEFT) {
if (action == GLFW_PRESS) {
if (gHoveredWidget) {
gDraggedWidget = gHoveredWidget;
gDraggedWidget->onDragStart();
}
}
else if (action == GLFW_RELEASE) {
if (gDraggedWidget) {
Widget *dropped = gScene->pick(gMousePos);
if (dropped) {
dropped->onDragDrop(gDraggedWidget);
}
gDraggedWidget->onDragEnd();
gDraggedWidget = NULL;
}
}
}
}

void cursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
Vec mousePos = Vec(xpos, ypos);
Vec mouseRel = mousePos.minus(gMousePos);
gMousePos = mousePos;

if (glfwGetInputMode(window, GLFW_CURSOR) != GLFW_CURSOR_DISABLED) {
// TODO Lock gMousePos
}

// onScroll
int middleButton = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE);
if (middleButton == GLFW_PRESS) {
gScene->scrollWidget->onScroll(mouseRel.neg());
}

Widget *hovered = gScene->pick(mousePos);

if (gDraggedWidget) {
// onDragMove
// Drag slower if Ctrl is held
bool fine = glfwGetKey(window, GLFW_KEY_LEFT_CONTROL ) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
float factor = fine ? 0.1 : 1.0;
gDraggedWidget->onDragMove(mouseRel.mult(factor));
// onDragHover
if (hovered) {
hovered->onDragHover(gDraggedWidget);
}
}
else {
// onMouseEnter and onMouseLeave
if (hovered != gHoveredWidget) {
if (gHoveredWidget) {
gHoveredWidget->onMouseLeave();
}
if (hovered) {
hovered->onMouseEnter();
}
}
gHoveredWidget = hovered;

// onMouseMove
if (hovered) {
hovered->onMouseMove(mouseRel);
}
}
}

void cursorEnterCallback(GLFWwindow* window, int entered) {
if (!entered) {
if (gHoveredWidget) {
gHoveredWidget->onMouseLeave();
}
gHoveredWidget = NULL;
}
}

void scrollCallback(GLFWwindow *window, double x, double y) {
Vec scrollRel = Vec(x, y);
// onScroll
gScene->scrollWidget->onScroll(scrollRel.mult(-95));
}

void charCallback(GLFWwindow *window, unsigned int value) {
}

static int lastWindowX, lastWindowY, lastWindowWidth, lastWindowHeight;

void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (action == GLFW_PRESS) {
if (key == GLFW_KEY_F11 || key == GLFW_KEY_ESCAPE) {
// Toggle fullscreen
GLFWmonitor *monitor = glfwGetWindowMonitor(window);
if (monitor) {
// Window mode
glfwSetWindowMonitor(window, NULL, lastWindowX, lastWindowY, lastWindowWidth, lastWindowHeight, 0);
}
else {
// Fullscreen
glfwGetWindowPos(window, &lastWindowX, &lastWindowY);
glfwGetWindowSize(window, &lastWindowWidth, &lastWindowHeight);
monitor = glfwGetPrimaryMonitor();
assert(monitor);
const GLFWvidmode *mode = glfwGetVideoMode(monitor);
glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
}
}
}
}

void renderGui() {
int width, height;
glfwGetFramebufferSize(window, &width, &height);

// Update and render
glViewport(0, 0, width, height);
glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

nvgBeginFrame(vg, width, height, 1.0);

gScene->draw(vg);
nvgEndFrame(vg);
glfwSwapBuffers(window);
}

void guiInit() {
int err;

// Set up GLFW
err = glfwInit();
assert(err);

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
window = glfwCreateWindow(1020, 700, "5V", NULL, NULL);
assert(window);
glfwMakeContextCurrent(window);

glfwSetWindowSizeCallback(window, windowSizeCallback);
glfwSetMouseButtonCallback(window, mouseButtonCallback);
// glfwSetCursorPosCallback(window, cursorPosCallback);
glfwSetCursorEnterCallback(window, cursorEnterCallback);
glfwSetScrollCallback(window, scrollCallback);
glfwSetCharCallback(window, charCallback);
glfwSetKeyCallback(window, keyCallback);

// Set up GLEW
glewExperimental = GL_TRUE;
err = glewInit();
assert(err == GLEW_OK);
// GLEW generates GL error because it calls glGetString(GL_EXTENSIONS), we'll consume it here.
glGetError();


// Set up NanoVG
vg = nvgCreateGL2(NVG_ANTIALIAS);
assert(vg);

// Set up Blendish
bndSetFont(loadFont("res/DejaVuSans.ttf"));
bndSetIconImage(loadImage("res/blender_icons16.png"));
}

void guiDestroy() {
nvgDeleteGL2(vg);
glfwDestroyWindow(window);
glfwTerminate();
}

void guiRun() {
assert(window);
{
int width, height;
glfwGetWindowSize(window, &width, &height);
windowSizeCallback(window, width, height);
}
double lastTime = 0.0;
while(!glfwWindowShouldClose(window)) {
glfwPollEvents();
{
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
cursorPosCallback(window, xpos, ypos);
}
gScene->step();
renderGui();

double currTime = glfwGetTime();
// printf("%lf fps\n", 1.0/(currTime - lastTime));
lastTime = currTime;
(void) lastTime;
}
}

void guiCursorLock() {
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}

void guiCursorUnlock() {
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}



std::map<std::string, int> images;
std::map<std::string, int> fonts;

int loadImage(std::string filename) {
assert(vg);
int imageId;
auto it = images.find(filename);
if (it == images.end()) {
// Load image
imageId = nvgCreateImage(vg, filename.c_str(), NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY);
if (imageId == 0) {
printf("Failed to load image %s\n", filename.c_str());
}
else {
printf("Loaded image %s\n", filename.c_str());
}
images[filename] = imageId;
}
else {
imageId = it->second;
}
return imageId;
}

int loadFont(std::string filename) {
assert(vg);
int fontId;
auto it = fonts.find(filename);
if (it == fonts.end()) {
fontId = nvgCreateFont(vg, filename.c_str(), filename.c_str());
if (fontId < 0) {
printf("Failed to load font %s\n", filename.c_str());
}
else {
printf("Loaded font %s\n", filename.c_str());
}
fonts[filename] = fontId;
}
else {
fontId = it->second;
}
return fontId;
}


void drawImage(NVGcontext *vg, Vec pos, int imageId) {
int width, height;
nvgImageSize(vg, imageId, &width, &height);
NVGpaint paint = nvgImagePattern(vg, pos.x, pos.y, width, height, 0, imageId, 1.0);
nvgFillPaint(vg, paint);
nvgRect(vg, pos.x, pos.y, width, height);
nvgFill(vg);
}

+ 4
- 0
src/gui.cpp.d View File

@@ -0,0 +1,4 @@
../src/gui.cpp.o: ../src/gui.cpp ../src/5V.hpp ../src/widgets.hpp \
../src/../lib/nanovg/src/nanovg.h ../src/../lib/oui/blendish.h \
../src/rack.hpp ../src/util.hpp ../src/../lib/nanovg/src/nanovg_gl.h \
../src/../lib/nanovg/src/nanovg.h

+ 32
- 0
src/main.cpp View File

@@ -0,0 +1,32 @@
#include "5V.hpp"
#include <time.h>


Scene *gScene = NULL;
RackWidget *gRackWidget = NULL;


int main() {
pluginInit();
rackInit();
guiInit();
gScene = new Scene();
// audioInit();
// audioDeviceOpen();
// midiInit();
rackStart();

// Blocks until user exits
guiRun();

// Cleanup
// midiDestroy();
// audioDeviceClose();
// audioDestroy();
delete gScene;
guiDestroy();
rackStop();
rackDestroy();
pluginDestroy();
return 0;
}

+ 98
- 0
src/plugin.cpp View File

@@ -0,0 +1,98 @@
#include <stdio.h>

#if defined(WINDOWS)
#include <windows.h>
#elif defined(LINUX) || defined(APPLE)
#include <dlfcn.h>
#include <glob.h>
#endif

#include "5V.hpp"
#include "core/core.hpp"


std::list<Plugin*> gPlugins;

int loadPlugin(const char *path) {
// Load dynamic/shared library
#if defined(WINDOWS)
HINSTANCE handle = LoadLibrary(path);
if (!handle) {
fprintf(stderr, "Failed to load library %s\n", path);
return -1;
}
#elif defined(LINUX) || defined(APPLE)
char ppath[512];
snprintf(ppath, 512, "./%s", path);
void *handle = dlopen(ppath, RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
fprintf(stderr, "Failed to load library %s: %s\n", path, dlerror());
return -1;
}
#endif

// Call plugin init() function
typedef Plugin *(*InitCallback)();
InitCallback initCallback;
#if defined(WINDOWS)
initCallback = (InitCallback) GetProcAddress(handle, "init");
#elif defined(LINUX) || defined(APPLE)
initCallback = (InitCallback) dlsym(handle, "init");
#endif
if (!initCallback) {
fprintf(stderr, "Failed to read init() symbol in %s\n", path);
return -2;
}

// Add plugin to map
Plugin *plugin = initCallback();
if (!plugin) {
fprintf(stderr, "Library %s did not return a plugin\n", path);
return -3;
}
gPlugins.push_back(plugin);
fprintf(stderr, "Loaded plugin %s\n", path);
return 0;
}

void pluginInit() {
// Load core
Plugin *corePlugin = coreInit();
gPlugins.push_back(corePlugin);

// Search for plugin libraries
#if defined(WINDOWS)
// WIN32_FIND_DATA ffd;
// HANDLE hFind = FindFirstFile("plugins/*/plugin.dll", &ffd);
// if (hFind != INVALID_HANDLE_VALUE) {
// do {
// loadPlugin(ffd.cFileName);
// } while (FindNextFile(hFind, &ffd));
// }
// FindClose(hFind);
loadPlugin("plugins/Simple/plugin.dll");
loadPlugin("plugins/AudibleInstruments/plugin.dll");

#elif defined(LINUX) || defined(APPLE)
glob_t result;
glob("plugins/*/plugin.so", GLOB_TILDE, NULL, &result);
for (int i = 0; i < (int) result.gl_pathc; i++) {
loadPlugin(result.gl_pathv[i]);
}
globfree(&result);
#endif
}

void pluginDestroy() {
for (Plugin *plugin : gPlugins) {
delete plugin;
}
gPlugins.clear();
}


Plugin::~Plugin() {
for (Model *model : models) {
delete model;
}
}

+ 3
- 0
src/plugin.cpp.d View File

@@ -0,0 +1,3 @@
../src/plugin.cpp.o: ../src/plugin.cpp ../src/5V.hpp ../src/widgets.hpp \
../src/../lib/nanovg/src/nanovg.h ../src/../lib/oui/blendish.h \
../src/rack.hpp ../src/util.hpp ../src/core/core.hpp

+ 180
- 0
src/rack.cpp View File

@@ -0,0 +1,180 @@
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <list>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "rack.hpp"


static std::thread thread;
static std::mutex mutex;
static std::condition_variable cv;
static long frame;
static long frameLimit;
static bool running;

static std::set<Module*> modules;
// Merely used for keeping track of which module inputs point to which module outputs, to prevent pointer mistakes and make the rack API rigorous
static std::set<Wire*> wires;


static Module *smoothModule = NULL;
static int smoothParamId;
static float smoothValue;


void rackInit() {
}

void rackDestroy() {
// Make sure there are no wires or modules in the rack on destruction. This suggests that a module failed to remove itself when the GUI was destroyed.
assert(wires.empty());
assert(modules.empty());
}

void rackStep() {
// Param interpolation
if (smoothModule) {
float value = smoothModule->params[smoothParamId];
const float minSpeed = 0.01 * 60.0 / SAMPLE_RATE; // Roughly 0.01 every graphics frame
const float lpCoeff = 60.0 / SAMPLE_RATE / 1.0; // decay rate is 1 graphics frame
float delta = smoothValue - value;
float speed = fmaxf(fabsf(delta) * lpCoeff, minSpeed);

if (delta < 0) {
value -= speed;
if (value < smoothValue) value = smoothValue;
}
else if (delta > 0) {
value += speed;
if (value > smoothValue) value = smoothValue;
}

smoothModule->params[smoothParamId] = value;
if (value == smoothValue) {
smoothModule = NULL;
}
}
// Step all modules
for (Module *module : modules) {
module->step();
}
}

void rackRun() {
while (1) {
std::unique_lock<std::mutex> lock(mutex);
if (!running)
break;
if (frame >= frameLimit) {
// Delay for at most 1ms if there are no needed frames
cv.wait_for(lock, std::chrono::milliseconds(1));
}
frame++;
lock.unlock();
// Speed up
// for (int i = 0; i < 16; i++)
rackStep();
}
}

void rackStart() {
frame = 0;
frameLimit = 0;
running = true;
thread = std::thread(rackRun);
}

void rackStop() {
{
std::unique_lock<std::mutex> lock(mutex);
running = false;
}
cv.notify_all();
thread.join();
}

void rackAddModule(Module *module) {
assert(module);
// Check that the module is not already added
assert(modules.find(module) == modules.end());
modules.insert(module);
}

void rackRemoveModule(Module *module) {
assert(module);
// Remove parameter interpolation which point to this module
if (module == smoothModule) {
smoothModule = NULL;
}
// FIXME use a mutex here
// Check that all wires are disconnected
for (Wire *wire : wires) {
assert(wire->outputModule != module);
assert(wire->inputModule != module);
}
auto it = modules.find(module);
if (it != modules.end()) {
modules.erase(it);
}
}

void rackConnectWire(Wire *wire) {
assert(wire);
// It would probably be good to reset the wire voltage
wire->value = 0.0;
// Check that the wire is not already added
assert(wires.find(wire) == wires.end());
assert(wire->outputModule);
assert(wire->inputModule);
// Check that the inputs/outputs are not already used by another cable
for (Wire *wire2 : wires) {
assert(wire2 != wire);
assert(!(wire2->outputModule == wire->outputModule && wire2->outputId == wire->outputId));
assert(!(wire2->inputModule == wire->inputModule && wire2->inputId == wire->inputId));
}
// Connect the wire to inputModule
wires.insert(wire);
wire->inputModule->inputs[wire->inputId] = &wire->value;
wire->outputModule->outputs[wire->outputId] = &wire->value;
}

void rackDisconnectWire(Wire *wire) {
assert(wire);
// Disconnect wire from inputModule
wire->inputModule->inputs[wire->inputId] = NULL;
wire->outputModule->outputs[wire->outputId] = NULL;

auto it = wires.find(wire);
assert(it != wires.end());
wires.erase(it);
}

long rackGetFrame() {
return frame;
}

void rackRequestFrame(long f) {
std::unique_lock<std::mutex> lock(mutex);
if (f > frameLimit) {
frameLimit = f;
lock.unlock();
cv.notify_all();
}
}

void rackSetParamSmooth(Module *module, int paramId, float value) {
// Check existing parameter interpolation
if (smoothModule) {
if (!(smoothModule == module && smoothParamId == paramId)) {
// Jump param value to smooth value
smoothModule->params[smoothParamId] = smoothValue;
}
}
smoothModule = module;
smoothParamId = paramId;
smoothValue = value;
}

+ 1
- 0
src/rack.cpp.d View File

@@ -0,0 +1 @@
../src/rack.cpp.o: ../src/rack.cpp ../src/rack.hpp

+ 34
- 0
src/rack.hpp View File

@@ -0,0 +1,34 @@
#pragma once

#include <stdlib.h>
#include <set>
#include <vector>

// TODO Find a clean way to make this a variable
#define SAMPLE_RATE 44100


struct Wire;

struct Module {
std::vector<float> params;
// Pointers to voltage values at each port
// If value is NULL, the input/output is disconnected
std::vector<float*> inputs;
std::vector<float*> outputs;

virtual ~Module() {}

// Always called on each sample frame before calling getOutput()
virtual void step() {}
};


struct Wire {
Module *outputModule = NULL;
int outputId;
Module *inputModule = NULL;
int inputId;
// The voltage which is pointed to by module inputs/outputs
float value = 0.0;
};

+ 117
- 0
src/util.hpp View File

@@ -0,0 +1,117 @@
#pragma once

#include <math.h>

////////////////////
// Utilities
// A header-only file with handy inline functions
////////////////////

#ifndef M_PI
#define M_PI 3.141592653589793238462643383
#define M_E 2.718281828459045235360287471
#endif

inline float clampf(float x, float min, float max) {
return fmaxf(min, fminf(max, x));
}

inline float mapf(float x, float xMin, float xMax, float yMin, float yMax) {
return yMin + (x - xMin) / (xMax - xMin) * (yMax - yMin);
}

inline float crossf(float a, float b, float frac) {
return (1 - frac) * a + frac * b;
}

inline int mini(int a, int b) {
return a < b ? a : b;
}

inline int maxi(int a, int b) {
return a > b ? a : b;
}

// Euclidean modulus, always returns 0 <= mod < base for positive base
// Assumes this architecture's division is non-Euclidean
inline int eucMod(int a, int base) {
int mod = a % base;
return mod < 0 ? mod + base : mod;
}

inline float getf(float *p, float v=0.0) {
return p ? *p : v;
}

inline void setf(float *p, float v) {
if (p) *p = v;
}


struct Vec {
float x, y;

Vec() : x(0.0), y(0.0) {}
Vec(float x, float y) : x(x), y(y) {}

Vec neg() {
return Vec(-x, -y);
}
Vec plus(Vec b) {
return Vec(x + b.x, y + b.y);
}
Vec minus(Vec b) {
return Vec(x - b.x, y - b.y);
}
Vec mult(float s) {
return Vec(x * s, y * s);
}
Vec div(float s) {
return Vec(x / s, y / s);
}
float dot(Vec b) {
return x * b.x + y * b.y;
}
float norm() {
return hypotf(x, y);
}
Vec min(Vec b) {
return Vec(fminf(x, b.x), fminf(y, b.y));
}
Vec max(Vec b) {
return Vec(fmaxf(x, b.x), fmaxf(y, b.y));
}
Vec round() {
return Vec(roundf(x), roundf(y));
}
};


struct Rect {
Vec pos;
Vec size;

Rect() {}
Rect(Vec pos, Vec size) : pos(pos), size(size) {}

bool contains(Vec v) {
return pos.x <= v.x && v.x < pos.x + size.x
&& pos.y <= v.y && v.y < pos.y + size.y;
}
bool intersects(Rect r) {
return (pos.x + size.x > r.pos.x && r.pos.x + r.size.x > pos.x)
&& (pos.y + size.y > r.pos.y && r.pos.y + r.size.y > pos.y);
}
Vec getCenter() {
return pos.plus(size.mult(0.5));
}
Vec getTopRight() {
return pos.plus(Vec(size.x, 0.0));
}
Vec getBottomLeft() {
return pos.plus(Vec(0.0, size.y));
}
Vec getBottomRight() {
return pos.plus(size);
}
};

+ 398
- 0
src/widgets.hpp View File

@@ -0,0 +1,398 @@
#pragma once

#include <assert.h>
#include <stdio.h>
#include <math.h>
#include <list>
#include <map>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <jansson.h>

#include "../lib/nanovg/src/nanovg.h"
#include "../lib/oui/blendish.h"

#include "rack.hpp"
#include "util.hpp"


struct MenuEntry;
struct RackWidget;
struct ParamWidget;
struct InputPort;
struct OutputPort;

////////////////////
// base class and traits
////////////////////

// A node in the 2D scene graph
struct Widget {
// Stores position and size
Rect box = Rect(Vec(0, 0), Vec(INFINITY, INFINITY));
Widget *parent = NULL;
std::list<Widget*> children;

virtual ~Widget();

Vec getAbsolutePos();
Rect getChildrenBoundingBox();

// Gives ownership of widget to this widget instance
void addChild(Widget *widget);
// Does not delete widget but transfers ownership to caller
// Silenty fails if widget is not a child
void removeChild(Widget *widget);
void clearChildren();

// Advances the module by one frame
virtual void step();
// Draws to NanoVG context
virtual void draw(NVGcontext *vg);

// Override this to return NULL if the widget is to be "invisible" to mouse events.
// Override this to return `this` if the widget is to override events of its children.
virtual Widget *pick(Vec pos);

// Events

virtual void onMouseDown(int button) {}
virtual void onMouseUp(int button) {}
virtual void onMouseMove(Vec mouseRel) {}
virtual void onMouseEnter() {}
virtual void onMouseLeave() {}
virtual void onScroll(Vec scrollRel) {}
virtual void onDragStart() {}
virtual void onDragDrop(Widget *origin) {}
virtual void onDragHover(Widget *origin) {}
virtual void onDragMove(Vec mouseRel) {}
virtual void onDragEnd() {}
virtual void onResize() {}
virtual void onAction() {}
virtual void onChange() {}
};

// Widget that does not respond to events, but allows its children to
struct TranslucentWidget : virtual Widget {
Widget *pick(Vec pos) {
Widget *picked = Widget::pick(pos);
if (picked == this) {
return NULL;
}
return picked;
}
};

// Widget that does not respond to events
struct TransparentWidget : virtual Widget {
Widget *pick(Vec pos) { return NULL; }
};

struct SpriteWidget : virtual Widget {
Vec spriteOffset;
Vec spriteSize;
std::string spriteFilename;
int index = 0;
void draw(NVGcontext *vg);
};

struct QuantityWidget : virtual Widget {
float value = 0.0;
float minValue = 0.0;
float maxValue = 1.0;
float defaultValue = 0.0;
std::string label;
// Include a space character if you want a space after the number, e.g. " Hz"
std::string unit;

void setValue(float value);
void setLimits(float minValue, float maxValue);
void setDefaultValue(float defaultValue);
};

////////////////////
// gui elements
////////////////////

struct Label : TransparentWidget {
std::string text;
void draw(NVGcontext *vg);
};

// Deletes itself from parent when clicked
struct MenuOverlay : Widget {
void onMouseDown(int button);
};

struct Menu : Widget {
Menu() {
box.size = Vec(0, 0);
}
// Transfers ownership, like addChild()
void pushChild(Widget *child);
void draw(NVGcontext *vg);
};

struct MenuEntry : Widget {
std::string text;
MenuEntry() {
box.size = Vec(0, BND_WIDGET_HEIGHT);
}
float computeMinWidth(NVGcontext *vg);
};

struct MenuLabel : MenuEntry {
void draw(NVGcontext *vg);
};

struct MenuItem : MenuEntry {
BNDwidgetState state = BND_DEFAULT;

void draw(NVGcontext *vg);

void onMouseUp(int button);
void onMouseEnter();
void onMouseLeave() ;
};

struct Button : Widget {
std::string text;
BNDwidgetState state = BND_DEFAULT;

Button();
void draw(NVGcontext *vg);
void onMouseEnter();
void onMouseLeave() ;
void onDragDrop(Widget *origin);
};

struct ChoiceButton : Button {
void draw(NVGcontext *vg);
};

struct Slider : QuantityWidget {
BNDwidgetState state = BND_DEFAULT;

Slider();
void draw(NVGcontext *vg);
void onDragStart();
void onDragMove(Vec mouseRel);
void onDragEnd();
};

struct ScrollBar : Widget {
enum { VERTICAL, HORIZONTAL } orientation;
float containerOffset = 0.0;
float containerSize = 0.0;
BNDwidgetState state = BND_DEFAULT;

ScrollBar();
void draw(NVGcontext *vg);
void move(float delta);
void onDragStart();
void onDragMove(Vec mouseRel);
void onDragEnd();
};

// Handles a container with scrollbars
struct ScrollWidget : Widget {
Widget *container;
ScrollBar *hScrollBar;
ScrollBar *vScrollBar;

ScrollWidget();
void draw(NVGcontext *vg);
void onResize();
void onScroll(Vec scrollRel);
};

struct Tooltip : TransparentWidget {
void step();
void draw(NVGcontext *vg);
};

////////////////////
// module
////////////////////

// A 1U module should be 15x380. Thus the width of a module should be a factor of 15.
struct Model;
struct ModuleWidget : Widget {
Model *model = NULL;
// Eventually this should be replaced with a `moduleId` which will be used for inter-process communication between the gui world and the audio world.
Module *module = NULL;
// int moduleId;

std::vector<InputPort*> inputs;
std::vector<OutputPort*> outputs;
std::vector<ParamWidget*> params;

ModuleWidget(Module *module);
~ModuleWidget();
json_t *toJson();
void fromJson(json_t *root);
void disconnectPorts();
void resetParams();
void cloneParams(ModuleWidget *source);

void draw(NVGcontext *vg);

bool requested = false;
Vec requestedPos;
Vec dragPos;
void onDragStart();
void onDragMove(Vec mouseRel);
void onDragEnd();
void onMouseDown(int button);
};

struct WireWidget : Widget {
OutputPort *outputPort = NULL;
InputPort *inputPort = NULL;
Wire *wire = NULL;
NVGcolor color;

WireWidget();
~WireWidget();
void updateWire();
void draw(NVGcontext *vg);
void drawOutputPlug(NVGcontext *vg);
void drawInputPlug(NVGcontext *vg);
};

struct RackWidget : Widget {
// Only put ModuleWidgets in here
Widget *moduleContainer;
// Only put WireWidgets in here
Widget *wireContainer;
// An unowned reference to the currently dragged wire
WireWidget *activeWire = NULL;

RackWidget();

void clear();
json_t *toJson();
void fromJson(json_t *root);
void repositionModule(ModuleWidget *module);
void step();
void draw(NVGcontext *vg);

void onMouseDown(int button);
};

////////////////////
// params
////////////////////

struct Light : TransparentWidget, SpriteWidget {
NVGcolor color;
void draw(NVGcontext *vg);
};

// If you don't add these to your ModuleWidget, it will fall out of the RackWidget
struct Screw : TransparentWidget, SpriteWidget {
Screw();
};

struct ParamWidget : QuantityWidget {
// Ancestor ModuleWidget, used for accessing the Module
ModuleWidget *moduleWidget;
int paramId;

json_t *toJson();
void fromJson(json_t *root);
void onMouseDown(int button);
void onChange();
};

struct Knob : ParamWidget, SpriteWidget {
int minIndex, maxIndex, spriteCount;
void step();
void onDragStart();
void onDragMove(Vec mouseRel);
void onDragEnd();
};

struct Switch : ParamWidget, SpriteWidget {
};

struct ToggleSwitch : virtual Switch {
void onDragStart() {
index = 1;
}
void onDragEnd() {
index = 0;
}
void onDragDrop(Widget *origin) {
if (origin == this) {
// Cycle through modes
// e.g. a range of [0.0, 3.0] would have modes 0, 1, 2, and 3.
float v = value + 1.0;
setValue(v > maxValue ? minValue : v);
}
}
};

struct MomentarySwitch : virtual Switch {
void onDragStart() {
setValue(maxValue);
index = 1;
}
void onDragEnd() {
setValue(minValue);
index = 0;
}
};

////////////////////
// ports
////////////////////

struct Port : Widget {
// Ancestor ModuleWidget, used for accessing the Module
ModuleWidget *moduleWidget;
WireWidget *connectedWire = NULL;

Port();
~Port();
void disconnect();

int type;
void draw(NVGcontext *vg);
void drawGlow(NVGcontext *vg);
void onDragEnd();
};

struct InputPort : Port {
int inputId;

void draw(NVGcontext *vg);
void onDragStart();
void onDragDrop(Widget *origin);
};

struct OutputPort : Port {
int outputId;

void draw(NVGcontext *vg);
void onDragStart();
void onDragDrop(Widget *origin);
};

////////////////////
// scene
////////////////////

struct Toolbar : Widget {
Slider *wireOpacitySlider;
Toolbar();
void draw(NVGcontext *vg);
};

struct Scene : Widget {
Toolbar *toolbar;
ScrollWidget *scrollWidget;
Scene();
void onResize();
};

+ 24
- 0
src/widgets/Button.cpp View File

@@ -0,0 +1,24 @@
#include "../5V.hpp"


Button::Button() {
box.size.y = BND_WIDGET_HEIGHT;
}

void Button::draw(NVGcontext *vg) {
bndToolButton(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str());
}

void Button::onMouseEnter() {
state = BND_HOVER;
}

void Button::onMouseLeave() {
state = BND_DEFAULT;
}

void Button::onDragDrop(Widget *origin) {
if (origin == this) {
onAction();
}
}

+ 6
- 0
src/widgets/ChoiceButton.cpp View File

@@ -0,0 +1,6 @@
#include "../5V.hpp"


void ChoiceButton::draw(NVGcontext *vg) {
bndChoiceButton(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str());
}

+ 34
- 0
src/widgets/InputPort.cpp View File

@@ -0,0 +1,34 @@
#include "../5V.hpp"


void InputPort::draw(NVGcontext *vg) {
Port::draw(vg);
if (gRackWidget->activeWire && gRackWidget->activeWire->inputPort) {
Port::drawGlow(vg);
}
}

void InputPort::onDragStart() {
if (connectedWire) {
// Disconnect wire from this port, but set it as the active wire
connectedWire->inputPort = NULL;
connectedWire->updateWire();
gRackWidget->activeWire = connectedWire;
connectedWire = NULL;
}
else {
connectedWire = new WireWidget();
connectedWire->inputPort = this;
gRackWidget->wireContainer->addChild(connectedWire);
gRackWidget->activeWire = connectedWire;
}
}

void InputPort::onDragDrop(Widget *origin) {
if (connectedWire) return;
if (gRackWidget->activeWire) {
if (gRackWidget->activeWire->inputPort) return;
gRackWidget->activeWire->inputPort = this;
connectedWire = gRackWidget->activeWire;
}
}

+ 21
- 0
src/widgets/Knob.cpp View File

@@ -0,0 +1,21 @@
#include "../5V.hpp"


#define KNOB_SENSITIVITY 0.001


void Knob::step() {
index = eucMod((int) roundf(mapf(value, minValue, maxValue, minIndex, maxIndex)), spriteCount);
}

void Knob::onDragStart() {
guiCursorLock();
}

void Knob::onDragMove(Vec mouseRel) {
setValue(value - KNOB_SENSITIVITY * (maxValue - minValue) * mouseRel.y);
}

void Knob::onDragEnd() {
guiCursorUnlock();
}

+ 6
- 0
src/widgets/Label.cpp View File

@@ -0,0 +1,6 @@
#include "../5V.hpp"


void Label::draw(NVGcontext *vg) {
bndLabel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, -1, text.c_str());
}

+ 35
- 0
src/widgets/Light.cpp View File

@@ -0,0 +1,35 @@
#include "../5V.hpp"


void Light::draw(NVGcontext *vg) {
SpriteWidget::draw(vg);

if (color.a == 0.0)
return;
// Draw glow
Vec c = box.getCenter();
float radius = box.size.x / 2;
NVGcolor icol, ocol;
NVGpaint paint;
// Inner glow
icol = nvgRGBf(1.0, 1.0, 1.0);
icol.a = clampf(color.a, 0.0, 1.0);
ocol = color;
ocol.a = clampf(color.a, 0.0, 1.0);
paint = nvgRadialGradient(vg, c.x+1, c.y+3, 0, radius, icol, ocol);
nvgFillPaint(vg, paint);
nvgBeginPath(vg);
nvgCircle(vg, c.x, c.y, radius);
nvgFill(vg);
// Outer glow
icol = color;
icol.a = clampf(0.1 * color.a, 0.0, 1.0);
ocol = color;
ocol.a = 0.0;
float oradius = radius + 20;
paint = nvgRadialGradient(vg, c.x, c.y, radius, oradius, icol, ocol);
nvgFillPaint(vg, paint);
nvgBeginPath(vg);
nvgRect(vg, c.x - oradius, c.y - oradius, 2*oradius, 2*oradius);
nvgFill(vg);
}

+ 27
- 0
src/widgets/Menu.cpp View File

@@ -0,0 +1,27 @@
#include "../5V.hpp"


void Menu::pushChild(Widget *child) {
child->box.pos = Vec(0, box.size.y);
addChild(child);
box.size.y += child->box.size.y;
}

void Menu::draw(NVGcontext *vg) {
// Resize the width to the widest child
for (Widget *child : children) {
MenuEntry *menuEntry = dynamic_cast<MenuEntry*>(child);
float width = menuEntry->computeMinWidth(vg);
if (width > box.size.x) {
box.size.x = width;
}
}
// Resize widths of children
for (Widget *child : children) {
child->box.size.x = box.size.x;
}

bndMenuBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE);

Widget::draw(vg);
}

+ 6
- 0
src/widgets/MenuEntry.cpp View File

@@ -0,0 +1,6 @@
#include "../5V.hpp"


float MenuEntry::computeMinWidth(NVGcontext *vg) {
return bndLabelWidth(vg, -1, text.c_str());
}

+ 26
- 0
src/widgets/MenuItem.cpp View File

@@ -0,0 +1,26 @@
#include "../5V.hpp"


void MenuItem::draw(NVGcontext *vg) {
bndMenuItem(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, state, -1, text.c_str());
}

void MenuItem::onMouseEnter() {
state = BND_HOVER;
}

void MenuItem::onMouseLeave() {
state = BND_DEFAULT;
}

void MenuItem::onMouseUp(int button) {
onAction();
// Remove overlay from scene
// HACK
Widget *overlay = parent->parent;
assert(overlay);
if (overlay->parent) {
overlay->parent->removeChild(overlay);
}
delete overlay;
}

+ 6
- 0
src/widgets/MenuLabel.cpp View File

@@ -0,0 +1,6 @@
#include "../5V.hpp"


void MenuLabel::draw(NVGcontext *vg) {
bndMenuLabel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, -1, text.c_str());
}

+ 10
- 0
src/widgets/MenuOverlay.cpp View File

@@ -0,0 +1,10 @@
#include "../5V.hpp"


void MenuOverlay::onMouseDown(int button) {
if (parent) {
parent->removeChild(this);
}
// Commit sudoku
delete this;
}

+ 163
- 0
src/widgets/ModuleWidget.cpp View File

@@ -0,0 +1,163 @@
#include "../5V.hpp"


ModuleWidget::ModuleWidget(Module *module) {
this->module = module;
if (module) {
rackAddModule(module);
}
}

ModuleWidget::~ModuleWidget() {
// Make sure WireWidget destructors are called *before* removing `module` from the rack.
disconnectPorts();
if (module) {
rackRemoveModule(module);
delete module;
}
}

json_t *ModuleWidget::toJson() {
json_t *root = json_object();

// plugin
json_object_set_new(root, "plugin", json_string(model->plugin->slug.c_str()));
// model
json_object_set_new(root, "model", json_string(model->slug.c_str()));
// pos
json_t *pos = json_pack("[f, f]", (double) box.pos.x, (double) box.pos.y);
json_object_set_new(root, "pos", pos);
// params
json_t *paramsJ = json_array();
for (ParamWidget *paramWidget : params) {
json_t *paramJ = paramWidget->toJson();
json_array_append_new(paramsJ, paramJ);
}
json_object_set_new(root, "params", paramsJ);

return root;
}

void ModuleWidget::fromJson(json_t *root) {
// pos
json_t *pos = json_object_get(root, "pos");
double x, y;
json_unpack(pos, "[F, F]", &x, &y);
box.pos = Vec(x, y);

// params
json_t *paramsJ = json_object_get(root, "params");
size_t paramId;
json_t *paramJ;
json_array_foreach(paramsJ, paramId, paramJ) {
params[paramId]->fromJson(paramJ);
}
}

void ModuleWidget::disconnectPorts() {
for (InputPort *input : inputs) {
input->disconnect();
}
for (OutputPort *output : outputs) {
output->disconnect();
}
}

void ModuleWidget::resetParams() {
for (ParamWidget *param : params) {
param->setValue(param->defaultValue);
}
}

void ModuleWidget::cloneParams(ModuleWidget *source) {
assert(params.size() == source->params.size());
for (size_t i = 0; i < params.size(); i++) {
params[i]->setValue(source->params[i]->value);
}
}

void ModuleWidget::draw(NVGcontext *vg) {
Widget::draw(vg);
bndBevel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);
}

void ModuleWidget::onDragStart() {
dragPos = gMousePos.minus(getAbsolutePos());
}

void ModuleWidget::onDragMove(Vec mouseRel) {
requestedPos = gMousePos.minus(parent->getAbsolutePos()).minus(dragPos);
requested = true;
}

void ModuleWidget::onDragEnd() {
}

struct DisconnectPortsMenuItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction() {
moduleWidget->disconnectPorts();
}
};

struct ResetParamsMenuItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction() {
moduleWidget->resetParams();
}
};

struct CloneModuleMenuItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction() {
// Create new module from model
ModuleWidget *clonedModuleWidget = moduleWidget->model->createModuleWidget();
clonedModuleWidget->requestedPos = moduleWidget->box.pos;
clonedModuleWidget->requested = true;
clonedModuleWidget->cloneParams(moduleWidget);
gRackWidget->moduleContainer->addChild(clonedModuleWidget);
}
};

struct DeleteModuleMenuItem : MenuItem {
ModuleWidget *moduleWidget;
void onAction() {
gRackWidget->moduleContainer->removeChild(moduleWidget);
delete moduleWidget;
}
};

void ModuleWidget::onMouseDown(int button) {
if (button == GLFW_MOUSE_BUTTON_RIGHT) {
MenuOverlay *overlay = new MenuOverlay();
Menu *menu = new Menu();
menu->box.pos = gMousePos;
{
MenuLabel *menuLabel = new MenuLabel();
menuLabel->text = model->plugin->name + ": " + model->name;
menu->pushChild(menuLabel);

ResetParamsMenuItem *resetItem = new ResetParamsMenuItem();
resetItem->text = "Initialize parameters";
resetItem->moduleWidget = this;
menu->pushChild(resetItem);

DisconnectPortsMenuItem *disconnectItem = new DisconnectPortsMenuItem();
disconnectItem->text = "Disconnect wires";
disconnectItem->moduleWidget = this;
menu->pushChild(disconnectItem);

CloneModuleMenuItem *cloneItem = new CloneModuleMenuItem();
cloneItem->text = "Clone";
cloneItem->moduleWidget = this;
menu->pushChild(cloneItem);

DeleteModuleMenuItem *deleteItem = new DeleteModuleMenuItem();
deleteItem->text = "Delete";
deleteItem->moduleWidget = this;
menu->pushChild(deleteItem);
}
overlay->addChild(menu);
gScene->addChild(overlay);
}
}

+ 34
- 0
src/widgets/OutputPort.cpp View File

@@ -0,0 +1,34 @@
#include "../5V.hpp"


void OutputPort::draw(NVGcontext *vg) {
Port::draw(vg);
if (gRackWidget->activeWire && gRackWidget->activeWire->outputPort) {
Port::drawGlow(vg);
}
}

void OutputPort::onDragStart() {
if (connectedWire) {
// Disconnect wire from this port, but set it as the active wire
connectedWire->outputPort = NULL;
connectedWire->updateWire();
gRackWidget->activeWire = connectedWire;
connectedWire = NULL;
}
else {
connectedWire = new WireWidget();
connectedWire->outputPort = this;
gRackWidget->wireContainer->addChild(connectedWire);
gRackWidget->activeWire = connectedWire;
}
}

void OutputPort::onDragDrop(Widget *origin) {
if (connectedWire) return;
if (gRackWidget->activeWire) {
if (gRackWidget->activeWire->outputPort) return;
gRackWidget->activeWire->outputPort = this;
connectedWire = gRackWidget->activeWire;
}
}

+ 29
- 0
src/widgets/ParamWidget.cpp View File

@@ -0,0 +1,29 @@
#include "../5V.hpp"


json_t *ParamWidget::toJson() {
json_t *paramJ = json_object();

json_t *valueJ = json_real(value);
json_object_set_new(paramJ, "value", valueJ);

return paramJ;
}

void ParamWidget::fromJson(json_t *root) {
json_t *valueJ = json_object_get(root, "value");
setValue(json_number_value(valueJ));
}

void ParamWidget::onMouseDown(int button) {
if (button == GLFW_MOUSE_BUTTON_RIGHT) {
setValue(defaultValue);
}
}

void ParamWidget::onChange() {
assert(moduleWidget);
assert(moduleWidget->module);
// moduleWidget->module->params[paramId] = value;
rackSetParamSmooth(moduleWidget->module, paramId, value);
}

+ 54
- 0
src/widgets/Port.cpp View File

@@ -0,0 +1,54 @@
#include "../5V.hpp"


Port::Port() {
box.size = Vec(20, 20);
type = rand() % 5;
}

Port::~Port() {
disconnect();
}

void Port::disconnect() {
if (connectedWire) {
gRackWidget->wireContainer->removeChild(connectedWire);
// On destruction, Wire automatically sets connectedWire to NULL
delete connectedWire;
}
}

void Port::draw(NVGcontext *vg) {
Vec pos = box.pos.plus(Vec(-18, -18));
int width, height;
int imageId = loadImage("res/port.png");
nvgImageSize(vg, imageId, &width, &height);
float offsetY = type * width;
NVGpaint paint = nvgImagePattern(vg, pos.x, pos.y - offsetY, width, height, 0.0, imageId, 1.0);
nvgFillPaint(vg, paint);
nvgBeginPath(vg);
nvgRect(vg, pos.x, pos.y, width, width);
nvgFill(vg);
}

void Port::drawGlow(NVGcontext *vg) {
Vec c = box.getCenter();
NVGcolor icol = nvgRGBAf(1, 1, 1, 0.5);
NVGcolor ocol = nvgRGBAf(1, 1, 1, 0);
NVGpaint paint = nvgRadialGradient(vg, c.x, c.y, 0, 20, icol, ocol);
nvgFillPaint(vg, paint);
nvgBeginPath(vg);
nvgRect(vg, box.pos.x - 10, box.pos.y - 10, box.size.x + 20, box.size.y + 20);
nvgFill(vg);
}

void Port::onDragEnd() {
WireWidget *w = gRackWidget->activeWire;
assert(w);
w->updateWire();
if (!w->wire) {
gRackWidget->wireContainer->removeChild(w);
delete w;
}
gRackWidget->activeWire = NULL;
}

+ 17
- 0
src/widgets/QuantityWidget.cpp View File

@@ -0,0 +1,17 @@
#include "../5V.hpp"


void QuantityWidget::setValue(float value) {
this->value = clampf(value, minValue, maxValue);
onChange();
}

void QuantityWidget::setLimits(float minValue, float maxValue) {
this->minValue = minValue;
this->maxValue = maxValue;
}

void QuantityWidget::setDefaultValue(float defaultValue) {
this->defaultValue = defaultValue;
setValue(defaultValue);
}

+ 264
- 0
src/widgets/RackWidget.cpp View File

@@ -0,0 +1,264 @@
#include "../5V.hpp"
#include <algorithm>


RackWidget::RackWidget() {
moduleContainer = new TranslucentWidget();
addChild(moduleContainer);

wireContainer = new TransparentWidget();
addChild(wireContainer);
}

void RackWidget::clear() {
activeWire = NULL;
wireContainer->clearChildren();
moduleContainer->clearChildren();
}

json_t *RackWidget::toJson() {
// root
json_t *root = json_object();

// rack
json_t *rack = json_object();
{
json_t *size = json_pack("[f, f]", (double) box.size.x, (double) box.size.y);
json_object_set_new(rack, "size", size);
}
json_object_set_new(root, "rack", rack);

// modules
json_t *modulesJ = json_array();
std::map<ModuleWidget*, int> moduleIds;
int moduleId = 0;
for (Widget *w : moduleContainer->children) {
ModuleWidget *moduleWidget = dynamic_cast<ModuleWidget*>(w);
assert(moduleWidget);
moduleIds[moduleWidget] = moduleId;
moduleId++;
// module
json_t *moduleJ = moduleWidget->toJson();
json_array_append_new(modulesJ, moduleJ);
}
json_object_set_new(root, "modules", modulesJ);

// wires
json_t *wires = json_array();
for (Widget *w : wireContainer->children) {
WireWidget *wireWidget = dynamic_cast<WireWidget*>(w);
assert(wireWidget);
// wire
json_t *wire = json_object();
{
int outputModuleId = moduleIds[wireWidget->outputPort->moduleWidget];
int inputModuleId = moduleIds[wireWidget->inputPort->moduleWidget];
json_object_set_new(wire, "outputModuleId", json_integer(outputModuleId));
json_object_set_new(wire, "outputId", json_integer(wireWidget->outputPort->outputId));
json_object_set_new(wire, "inputModuleId", json_integer(inputModuleId));
json_object_set_new(wire, "inputId", json_integer(wireWidget->inputPort->inputId));
}
json_array_append_new(wires, wire);
}
json_object_set_new(root, "wires", wires);

return root;
}

void RackWidget::fromJson(json_t *root) {
// TODO There's virtually no validation in here. Bad input will result in a crash.
// rack
json_t *rack = json_object_get(root, "rack");
assert(rack);
{
// size
json_t *size = json_object_get(rack, "size");
double width, height;
json_unpack(size, "[F, F]", &width, &height);
box.size = Vec(width, height);
}

// modules
std::map<int, ModuleWidget*> moduleWidgets;
json_t *modulesJ = json_object_get(root, "modules");
size_t moduleId;
json_t *moduleJ;
json_array_foreach(modulesJ, moduleId, moduleJ) {
// Get plugin
json_t *pluginSlugJ = json_object_get(moduleJ, "plugin");
const char *pluginSlug = json_string_value(pluginSlugJ);
Plugin *plugin = NULL;
for (Plugin *p : gPlugins) {
if (p->slug == pluginSlug) {
plugin = p;
break;
}
}
assert(plugin);

// Get model
json_t *modelSlug = json_object_get(moduleJ, "model");
Model *model = NULL;
for (Model *m : plugin->models) {
if (m->slug == json_string_value(modelSlug)) {
model = m;
break;
}
}
assert(model);

// Create ModuleWidget
ModuleWidget *moduleWidget = model->createModuleWidget();
moduleWidget->fromJson(moduleJ);
moduleContainer->addChild(moduleWidget);
moduleWidgets[moduleId] = moduleWidget;
}

// wires
json_t *wiresJ = json_object_get(root, "wires");
size_t wireId;
json_t *wireJ;
json_array_foreach(wiresJ, wireId, wireJ) {
int outputModuleId, outputId;
int inputModuleId, inputId;
json_unpack(wireJ, "{s:i, s:i, s:i, s:i}",
"outputModuleId", &outputModuleId, "outputId", &outputId,
"inputModuleId", &inputModuleId, "inputId", &inputId);
// Get ports
ModuleWidget *outputModuleWidget = moduleWidgets[outputModuleId];
assert(outputModuleWidget);
OutputPort *outputPort = outputModuleWidget->outputs[outputId];
assert(outputPort);
ModuleWidget *inputModuleWidget = moduleWidgets[inputModuleId];
assert(inputModuleWidget);
InputPort *inputPort = inputModuleWidget->inputs[inputId];
assert(inputPort);
// Create WireWidget
WireWidget *wireWidget = new WireWidget();
wireWidget->outputPort = outputPort;
wireWidget->inputPort = inputPort;
outputPort->connectedWire = wireWidget;
inputPort->connectedWire = wireWidget;
wireWidget->updateWire();
// Add wire to rack
gRackWidget->wireContainer->addChild(wireWidget);
}
}

void RackWidget::repositionModule(ModuleWidget *module) {
// Create possible positions
int x0 = roundf(module->requestedPos.x / 15);
int y0 = roundf(module->requestedPos.y / 380);
std::vector<Vec> positions;
for (int y = maxi(0, y0 - 2); y < y0 + 2; y++) {
for (int x = maxi(0, x0 - 40); x < x0 + 40; x++) {
positions.push_back(Vec(x*15, y*380));
}
}

// Sort possible positions by distance to the requested position
Vec requestedPos = module->requestedPos;
std::sort(positions.begin(), positions.end(), [requestedPos](Vec a, Vec b) {
return a.minus(requestedPos).norm() < b.minus(requestedPos).norm();
});

// Find a position that does not collide
for (Vec pos : positions) {
Rect newBox = Rect(pos, module->box.size);
bool collides = false;
for (Widget *child2 : moduleContainer->children) {
if (module == child2) continue;
if (newBox.intersects(child2->box)) {
collides = true;
break;
}
}
if (collides) continue;

module->box.pos = pos;
break;
}
}

void RackWidget::step() {
// Resize to be a bit larger than the ScrollWidget viewport
assert(parent);
assert(parent->parent);
Vec moduleSize = moduleContainer->getChildrenBoundingBox().getBottomRight();
Vec viewportSize = parent->parent->box.size.minus(parent->box.pos);
box.size = moduleSize.max(viewportSize).plus(Vec(500, 500));

// Reposition modules
for (Widget *child : moduleContainer->children) {
ModuleWidget *module = dynamic_cast<ModuleWidget*>(child);
assert(module);
if (module->requested) {
repositionModule(module);
module->requested = false;
}
}

Widget::step();
}

void RackWidget::draw(NVGcontext *vg) {
// Draw background
nvgBeginPath(vg);
nvgRect(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);
NVGpaint paint;
{
int imageId = loadImage("res/wood.jpg");
int imageWidth, imageHeight;
nvgImageSize(vg, imageId, &imageWidth, &imageHeight);
paint = nvgImagePattern(vg, box.pos.x, box.pos.y, imageWidth, imageHeight, 0.0, imageId, 1.0);
nvgFillPaint(vg, paint);
nvgFill(vg);
}
{
int imageId = loadImage("res/rackrails.png");
int imageWidth, imageHeight;
nvgImageSize(vg, imageId, &imageWidth, &imageHeight);
paint = nvgImagePattern(vg, box.pos.x, box.pos.y, imageWidth, imageHeight, 0.0, imageId, 1.0);
nvgFillPaint(vg, paint);
nvgFill(vg);
}

Widget::draw(vg);
}

struct AddModuleMenuItem : MenuItem {
Model *model;
Vec modulePos;
void onAction() {
ModuleWidget *moduleWidget = model->createModuleWidget();
moduleWidget->requestedPos = modulePos.minus(moduleWidget->box.getCenter());
moduleWidget->requested = true;
gRackWidget->moduleContainer->addChild(moduleWidget);
}
};

void RackWidget::onMouseDown(int button) {
if (button == GLFW_MOUSE_BUTTON_RIGHT) {
// Get relative position of the click
Vec modulePos = gMousePos.minus(getAbsolutePos());

MenuOverlay *overlay = new MenuOverlay();
Menu *menu = new Menu();
menu->box.pos = gMousePos;

MenuLabel *menuLabel = new MenuLabel();
menuLabel->text = "Add Module";
menu->pushChild(menuLabel);
for (Plugin *plugin : gPlugins) {
for (Model *model : plugin->models) {
AddModuleMenuItem *item = new AddModuleMenuItem();
item->text = model->plugin->name + ": " + model->name;
item->model = model;
item->modulePos = modulePos;
menu->pushChild(item);
}
}
overlay->addChild(menu);
gScene->addChild(overlay);
}
}

+ 22
- 0
src/widgets/Scene.cpp View File

@@ -0,0 +1,22 @@
#include "../5V.hpp"


Scene::Scene() {
scrollWidget = new ScrollWidget();
{
assert(!gRackWidget);
gRackWidget = new RackWidget();
scrollWidget->container->addChild(gRackWidget);
}
addChild(scrollWidget);

toolbar = new Toolbar();
addChild(toolbar);
scrollWidget->box.pos.y = toolbar->box.size.y;
}

void Scene::onResize() {
toolbar->box.size.x = box.size.x;
scrollWidget->box.size = box.size.minus(scrollWidget->box.pos);
scrollWidget->onResize();
}

+ 10
- 0
src/widgets/Screw.cpp View File

@@ -0,0 +1,10 @@
#include "../5V.hpp"


Screw::Screw() {
box.size = Vec(15, 15);
spriteOffset = Vec(-7, -7);
spriteSize = Vec(29, 29);
spriteFilename = "res/screw.png";
index = rand() % 5;
}

+ 38
- 0
src/widgets/ScrollBar.cpp View File

@@ -0,0 +1,38 @@
#include "../5V.hpp"


ScrollBar::ScrollBar() {
box.size.x = BND_SCROLLBAR_WIDTH;
box.size.y = BND_SCROLLBAR_HEIGHT;
}

void ScrollBar::draw(NVGcontext *vg) {
float boxSize = (orientation == VERTICAL ? box.size.y : box.size.x);
float maxOffset = containerSize - boxSize;
float offset = containerOffset / maxOffset;
float size = boxSize / containerSize;
size = clampf(size, 0.0, 1.0);
bndScrollBar(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, state, offset, size);
}

void ScrollBar::move(float delta) {
float boxSize = (orientation == VERTICAL ? box.size.y : box.size.x);
float maxOffset = containerSize - boxSize;
containerOffset += delta;
containerOffset = clampf(containerOffset, 0.0, maxOffset);
}

void ScrollBar::onDragStart() {
state = BND_ACTIVE;
guiCursorLock();
}

void ScrollBar::onDragMove(Vec mouseRel) {
float delta = (orientation == VERTICAL ? mouseRel.y : mouseRel.x);
move(delta);
}

void ScrollBar::onDragEnd() {
state = BND_DEFAULT;
guiCursorUnlock();
}

+ 42
- 0
src/widgets/ScrollWidget.cpp View File

@@ -0,0 +1,42 @@
#include "../5V.hpp"


ScrollWidget::ScrollWidget() {
container = new Widget();
addChild(container);

hScrollBar = new ScrollBar();
hScrollBar->orientation = ScrollBar::HORIZONTAL;
addChild(hScrollBar);

vScrollBar = new ScrollBar();
vScrollBar->orientation = ScrollBar::VERTICAL;
addChild(vScrollBar);
}

void ScrollWidget::draw(NVGcontext *vg) {
// Update the scrollbar sizes
Vec c = container->getChildrenBoundingBox().getBottomRight();
hScrollBar->containerSize = c.x;
vScrollBar->containerSize = c.y;

// Update the container's positions from the scrollbar offsets
container->box.pos = Vec(-hScrollBar->containerOffset, -vScrollBar->containerOffset).round();

Widget::draw(vg);
}

void ScrollWidget::onResize() {
Vec b = Vec(box.size.x - vScrollBar->box.size.x, box.size.y - hScrollBar->box.size.y);

hScrollBar->box.pos.y = b.y;
hScrollBar->box.size.x = b.x;

vScrollBar->box.pos.x = b.x;
vScrollBar->box.size.y = b.y;
}

void ScrollWidget::onScroll(Vec scrollRel) {
hScrollBar->move(scrollRel.x);
vScrollBar->move(scrollRel.y);
}

+ 29
- 0
src/widgets/Slider.cpp View File

@@ -0,0 +1,29 @@
#include "../5V.hpp"


#define SLIDER_SENSITIVITY 0.001

Slider::Slider() {
box.size.y = BND_WIDGET_HEIGHT;
}

void Slider::draw(NVGcontext *vg) {
float progress = mapf(value, minValue, maxValue, 0.0, 1.0);
char quantity[100];
snprintf(quantity, 100, "%.3g%s", value, unit.c_str());
bndSlider(vg, box.pos.x, box.pos.y, box.size.x, box.size.y, BND_CORNER_NONE, state, progress, label.c_str(), quantity);
}

void Slider::onDragStart() {
state = BND_ACTIVE;
guiCursorLock();
}

void Slider::onDragMove(Vec mouseRel) {
setValue(value + SLIDER_SENSITIVITY * (maxValue - minValue) * mouseRel.x);
}

void Slider::onDragEnd() {
state = BND_DEFAULT;
guiCursorUnlock();
}

+ 26
- 0
src/widgets/SpriteWidget.cpp View File

@@ -0,0 +1,26 @@
#include "../5V.hpp"


void SpriteWidget::draw(NVGcontext *vg) {
int imageId = loadImage(spriteFilename);
if (imageId < 0) {
// printf("Could not load image %s for SpriteWidget\n", spriteFilename.c_str());
return;
}

Vec pos = box.pos.plus(spriteOffset);

int width, height;
nvgImageSize(vg, imageId, &width, &height);
int stride = width / spriteSize.x;
if (stride == 0) {
printf("Width of SpriteWidget is %d but spriteSize is %f\n", width, spriteSize.x);
return;
}
Vec offset = Vec((index % stride) * spriteSize.x, (index / stride) * spriteSize.y);
NVGpaint paint = nvgImagePattern(vg, pos.x - offset.x, pos.y - offset.y, width, height, 0.0, imageId, 1.0);
nvgFillPaint(vg, paint);
nvgBeginPath(vg);
nvgRect(vg, pos.x, pos.y, spriteSize.x, spriteSize.y);
nvgFill(vg);
}

+ 171
- 0
src/widgets/Toolbar.cpp View File

@@ -0,0 +1,171 @@
#include "../5V.hpp"

extern "C" {
#include "../lib/noc/noc_file_dialog.h"
}


static const char *filters = "JSON Patch\0*.json\0";


struct NewItem : MenuItem {
void onAction() {
gRackWidget->clear();
}
};

struct SaveItem : MenuItem {
void onAction() {
const char *path = noc_file_dialog_open(NOC_FILE_DIALOG_SAVE, filters, NULL, "Untitled.json");
printf("Saving patch %s\n", path);

if (path) {
FILE *file = fopen(path, "w");

json_t *root = gRackWidget->toJson();
assert(root);
json_dumpf(root, file, JSON_INDENT(2));
json_decref(root);

fclose(file);
}
}
};

struct OpenItem : MenuItem {
void onAction() {
const char *path = noc_file_dialog_open(NOC_FILE_DIALOG_OPEN, filters, NULL, NULL);
printf("Loading patch %s\n", path);

if (path) {
FILE *file = fopen(path, "r");

json_error_t error;
json_t *root = json_loadf(file, 0, &error);
if (root) {
gRackWidget->clear();
gRackWidget->fromJson(root);
json_decref(root);
}
else {
printf("JSON parsing error at %s %d:%d %s\n", error.source, error.line, error.column, error.text);
}

fclose(file);
}
}
};

struct FileChoice : ChoiceButton {
void onAction() {
MenuOverlay *overlay = new MenuOverlay();
Menu *menu = new Menu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;
{
MenuItem *newItem = new NewItem();
newItem->text = "New";
menu->pushChild(newItem);

MenuItem *openItem = new OpenItem();
openItem->text = "Open";
menu->pushChild(openItem);

MenuItem *saveItem = new SaveItem();
saveItem->text = "Save";
menu->pushChild(saveItem);

MenuItem *saveAsItem = new SaveItem();
saveAsItem->text = "Save As";
menu->pushChild(saveAsItem);
}
overlay->addChild(menu);
gScene->addChild(overlay);
}
};


struct SampleRateItem : MenuItem {
float sampleRate;
void onAction() {
printf("\"\"\"\"\"\"\"\"switching\"\"\"\"\"\"\"\" sample rate to %f\n", sampleRate);
}
};

struct SampleRateChoice : ChoiceButton {
void onAction() {
MenuOverlay *overlay = new MenuOverlay();
Menu *menu = new Menu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;

float sampleRates[6] = {44100, 48000, 88200, 96000, 176400, 192000};
for (int i = 0; i < 6; i++) {
SampleRateItem *item = new SampleRateItem();
char text[100];
snprintf(text, 100, "%.0f Hz", sampleRates[i]);
item->text = std::string(text);
item->sampleRate = sampleRates[i];
menu->pushChild(item);
}

overlay->addChild(menu);
gScene->addChild(overlay);
}
};


Toolbar::Toolbar() {
float margin = 5;
box.size = Vec(1020, BND_WIDGET_HEIGHT + 2*margin);

float xPos = margin;
{
Label *label = new Label();
label->box.pos = Vec(xPos, margin);
label->text = "Rack v0.0.0 alpha";
addChild(label);
xPos += 150;
}

xPos += margin;
{
ChoiceButton *fileChoice = new FileChoice();
fileChoice->box.pos = Vec(xPos, margin);
fileChoice->box.size.x = 100;
fileChoice->text = "File";
addChild(fileChoice);
xPos += fileChoice->box.size.x;
}

xPos += margin;
{
SampleRateChoice *srChoice = new SampleRateChoice();
srChoice->box.pos = Vec(xPos, margin);
srChoice->box.size.x = 100;
// TODO Change to actual sample rate, e.g. 44100 Hz
srChoice->text = "Sample Rate";
addChild(srChoice);
xPos += srChoice->box.size.x;
}

xPos += margin;
{
wireOpacitySlider = new Slider();
wireOpacitySlider->box.pos = Vec(xPos, margin);
wireOpacitySlider->box.size.x = 150;
wireOpacitySlider->label = "Wire opacity";
wireOpacitySlider->unit = "%";
wireOpacitySlider->setLimits(0.0, 100.0);
wireOpacitySlider->setDefaultValue(100.0);
addChild(wireOpacitySlider);
xPos += wireOpacitySlider->box.size.x;
}
}

void Toolbar::draw(NVGcontext *vg) {
bndBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);
bndBevel(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);

Widget::draw(vg);
}

+ 16
- 0
src/widgets/Tooltip.cpp View File

@@ -0,0 +1,16 @@
#include "../5V.hpp"


void Tooltip::step() {
// Follow the mouse
box.pos = gMousePos.minus(parent->getAbsolutePos());

// Wrap size to contents
// box.size = getChildrenBoundingBox().getBottomRight();
}


void Tooltip::draw(NVGcontext *vg) {
bndTooltipBackground(vg, box.pos.x, box.pos.y, box.size.x, box.size.y);
Widget::draw(vg);
}

+ 89
- 0
src/widgets/Widget.cpp View File

@@ -0,0 +1,89 @@
#include "../5V.hpp"
#include <algorithm>


Widget::~Widget() {
// You should only delete orphaned widgets
assert(!parent);
// Stop dragging and hovering this widget
if (gHoveredWidget == this)
gHoveredWidget = NULL;
if (gDraggedWidget == this)
gDraggedWidget = NULL;
clearChildren();
}

Vec Widget::getAbsolutePos() {
// Recursively compute position offset from parents
if (!parent) {
return box.pos;
}
else {
return box.pos.plus(parent->getAbsolutePos());
}
}

Rect Widget::getChildrenBoundingBox() {
if (children.empty()) {
return Rect();
}

Vec topLeft = Vec(INFINITY, INFINITY);
Vec bottomRight = Vec(-INFINITY, -INFINITY);
for (Widget *child : children) {
topLeft = topLeft.min(child->box.pos);
bottomRight = bottomRight.max(child->box.getBottomRight());
}
return Rect(topLeft, bottomRight.minus(topLeft));
}

void Widget::addChild(Widget *widget) {
assert(!widget->parent);
widget->parent = this;
children.push_back(widget);
}

void Widget::removeChild(Widget *widget) {
assert(widget->parent == this);
auto it = std::find(children.begin(), children.end(), widget);
if (it != children.end()) {
children.erase(it);
widget->parent = NULL;
}
}

void Widget::clearChildren() {
for (Widget *child : children) {
child->parent = NULL;
delete child;
}
children.clear();
}

void Widget::step() {
for (Widget *child : children) {
child->step();
}
}

void Widget::draw(NVGcontext *vg) {
nvgSave(vg);
nvgTranslate(vg, box.pos.x, box.pos.y);
for (Widget *child : children) {
child->draw(vg);
}
nvgRestore(vg);
}

Widget *Widget::pick(Vec pos) {
if (!box.contains(pos))
return NULL;
pos = pos.minus(box.pos);
for (auto it = children.rbegin(); it != children.rend(); it++) {
Widget *child = *it;
Widget *picked = child->pick(pos);
if (picked)
return picked;
}
return this;
}

+ 112
- 0
src/widgets/WireWidget.cpp View File

@@ -0,0 +1,112 @@
#include "../5V.hpp"


void drawWire(NVGcontext *vg, Vec pos1, Vec pos2, NVGcolor color) {
float dist = pos1.minus(pos2).norm();
Vec slump = Vec(0, 100.0 + 0.5*dist);
Vec pos3 = pos1.plus(pos2).div(2).plus(slump);

NVGcolor colorOutline = nvgRGBf(0, 0, 0);

nvgLineJoin(vg, NVG_ROUND);
nvgStrokeWidth(vg, 4);
// Shadow
// Vec pos4 = pos3.plus(slump.mult(0.1));
// NVGcolor colorShadow = nvgRGBAf(0, 0, 0, 0.2);
// nvgBeginPath(vg);
// nvgMoveTo(vg, pos1.x, pos1.y);
// nvgQuadTo(vg, pos4.x, pos4.y, pos2.x, pos2.y);
// nvgStrokeColor(vg, colorShadow);
// nvgStroke(vg);

// Wire outline
nvgBeginPath(vg);
nvgMoveTo(vg, pos1.x, pos1.y);
nvgQuadTo(vg, pos3.x, pos3.y, pos2.x, pos2.y);
nvgStrokeColor(vg, colorOutline);
nvgStroke(vg);

// Wire solid
nvgStrokeWidth(vg, 2);
nvgStrokeColor(vg, color);
nvgStroke(vg);
}


static NVGcolor wireColors[8] = {
nvgRGB(0x50, 0x50, 0x50),
nvgRGB(0xac, 0x41, 0x42),
nvgRGB(0x90, 0xa9, 0x59),
nvgRGB(0xf4, 0xbf, 0x75),
nvgRGB(0x6a, 0x9f, 0xb5),
nvgRGB(0xaa, 0x75, 0x9f),
nvgRGB(0x75, 0xb5, 0xaa),
nvgRGB(0xf5, 0xf5, 0xf5),
};
static int wireColorId = 1;



WireWidget::WireWidget() {
wireColorId = (wireColorId + 1) % 8;
color = wireColors[wireColorId];
}

WireWidget::~WireWidget() {
if (outputPort) {
outputPort->connectedWire = NULL;
outputPort = NULL;
}
if (inputPort) {
inputPort->connectedWire = NULL;
inputPort = NULL;
}
updateWire();
}

void WireWidget::updateWire() {
if (wire) {
rackDisconnectWire(wire);
delete wire;
wire = NULL;
}
if (inputPort && outputPort) {
wire = new Wire();
wire->outputModule = outputPort->moduleWidget->module;
wire->outputId = outputPort->outputId;
wire->inputModule = inputPort->moduleWidget->module;
wire->inputId = inputPort->inputId;
rackConnectWire(wire);
}
}

void WireWidget::draw(NVGcontext *vg) {
Vec outputPos, inputPos;
Vec absolutePos = getAbsolutePos();

if (outputPort) {
outputPos = Rect(outputPort->getAbsolutePos(), outputPort->box.size).getCenter();
}
else {
outputPos = gMousePos;
}
if (inputPort) {
inputPos = Rect(inputPort->getAbsolutePos(), inputPort->box.size).getCenter();
}
else {
inputPos = gMousePos;
}

outputPos = outputPos.minus(absolutePos);
inputPos = inputPos.minus(absolutePos);

bndNodePort(vg, outputPos.x, outputPos.y, BND_DEFAULT, color);
bndNodePort(vg, inputPos.x, inputPos.y, BND_DEFAULT, color);
nvgSave(vg);
float wireOpacity = gScene->toolbar->wireOpacitySlider->value / 100.0;
if (wireOpacity > 0.0) {
nvgGlobalAlpha(vg, wireOpacity);
drawWire(vg, outputPos, inputPos, color);
}
nvgRestore(vg);
}

Loading…
Cancel
Save