@@ -1,94 +1,5 @@ | |||||
# VCV Rack | |||||
# Rack | |||||
*Rack* is the main application for the VCV open-source virtual modular synthesizer. | |||||
*Rack* is the engine for the VCV open-source virtual modular synthesizer. | |||||
This README includes instructions for building Rack from source. For information about the software, go to https://vcvrack.com/. | |||||
## The [Issue Tracker](https://github.com/VCVRack/Rack/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) is the official developer's forum | |||||
Bug reports, feature requests, questions, and discussions are welcome on the GitHub Issue Tracker for all repos under the VCVRack organization. | |||||
However, please search before posting to avoid duplicates, and limit to one issue per post. | |||||
Please vote on feature requests by using the Thumbs Up/Down reaction on the first post. | |||||
I rarely accept code contributions to Rack itself, so please notify me in advance if you wish to send a pull request. | |||||
## Setting up your development environment | |||||
Before building Rack, you must install build dependencies provided by your system's package manager. | |||||
Rack's own dependencies (GLEW, glfw, etc) do not need to be installed on your system, since specific versions are compiled locally during the build process. | |||||
However, you need proper tools to build Rack and these dependencies. | |||||
### Mac | |||||
Install [Xcode](https://developer.apple.com/xcode/). | |||||
Using [Homebrew](https://brew.sh/), install the build dependencies. | |||||
``` | |||||
brew install git wget cmake autoconf automake libtool | |||||
``` | |||||
### Windows | |||||
Install [MSYS2](http://www.msys2.org/) and launch the MinGW 64-bit shell (not the default MSYS shell). | |||||
``` | |||||
pacman -S git wget make tar unzip zip mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake autoconf automake mingw-w64-x86_64-libtool | |||||
``` | |||||
### Linux | |||||
On Ubuntu 16.04: | |||||
``` | |||||
sudo apt install git curl cmake libx11-dev libglu1-mesa-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev zlib1g-dev libasound2-dev libgtk2.0-dev libjack-jackd2-dev | |||||
``` | |||||
On Arch Linux: | |||||
``` | |||||
pacman -S git wget gcc make cmake tar unzip zip curl | |||||
``` | |||||
## Building | |||||
*If the build fails for you, please report the issue with a detailed error message to help the portability of Rack.* | |||||
Clone this repository with `git clone https://github.com/VCVRack/Rack.git` and `cd Rack`. | |||||
Make sure there are no spaces in your absolute path, as this breaks many build systems. | |||||
Clone submodules. | |||||
git submodule update --init --recursive | |||||
Build dependencies locally. | |||||
You may add `-j$(nproc)` to your make commands to parallelize builds across all CPU cores. | |||||
make dep | |||||
Build Rack. | |||||
make | |||||
Run Rack. | |||||
make run | |||||
## Building plugins | |||||
Be sure to check out and build the version of Rack you wish to build your plugins against. | |||||
You must clone the plugin in Rack's `plugins/` directory, e.g. | |||||
cd plugins | |||||
git clone https://github.com/VCVRack/Fundamental.git | |||||
Clone submodules. | |||||
cd Fundamental | |||||
git submodule update --init --recursive | |||||
Build plugin. | |||||
make dep | |||||
make | |||||
## Licenses | |||||
See [LICENSE.md](LICENSE.md) for a description of all licenses for VCV Rack. | |||||
For information about the software, go to the [VCV website](https://vcvrack.com/) or the [VCV Rack manual](https://vcvrack.com/manual/). |
@@ -10,38 +10,13 @@ namespace rack { | |||||
struct PanelBorder : TransparentWidget { | struct PanelBorder : TransparentWidget { | ||||
void draw(NVGcontext *vg) override { | |||||
NVGcolor borderColor = nvgRGBAf(0.5, 0.5, 0.5, 0.5); | |||||
nvgBeginPath(vg); | |||||
nvgRect(vg, 0.5, 0.5, box.size.x - 1.0, box.size.y - 1.0); | |||||
nvgStrokeColor(vg, borderColor); | |||||
nvgStrokeWidth(vg, 1.0); | |||||
nvgStroke(vg); | |||||
} | |||||
void draw(NVGcontext *vg) override; | |||||
}; | }; | ||||
struct SVGPanel : FramebufferWidget { | struct SVGPanel : FramebufferWidget { | ||||
void step() override { | |||||
if (math::isNear(app()->window->pixelRatio, 1.0)) { | |||||
// Small details draw poorly at low DPI, so oversample when drawing to the framebuffer | |||||
oversample = 2.0; | |||||
} | |||||
FramebufferWidget::step(); | |||||
} | |||||
void setBackground(std::shared_ptr<SVG> svg) { | |||||
SVGWidget *sw = new SVGWidget; | |||||
sw->setSVG(svg); | |||||
addChild(sw); | |||||
// Set size | |||||
box.size = sw->box.size.div(RACK_GRID_SIZE).round().mult(RACK_GRID_SIZE); | |||||
PanelBorder *pb = new PanelBorder; | |||||
pb->box.size = box.size; | |||||
addChild(pb); | |||||
} | |||||
void step() override; | |||||
void setBackground(std::shared_ptr<SVG> svg); | |||||
}; | }; | ||||
@@ -11,26 +11,26 @@ extern const std::string APP_NAME; | |||||
extern const std::string APP_VERSION; | extern const std::string APP_VERSION; | ||||
extern const std::string API_HOST; | extern const std::string API_HOST; | ||||
static const float SVG_DPI = 75.0; | |||||
static const float APP_SVG_DPI = 75.0; | |||||
static const float MM_PER_IN = 25.4; | static const float MM_PER_IN = 25.4; | ||||
/** Converts inch measurements to pixels */ | /** Converts inch measurements to pixels */ | ||||
inline float in2px(float in) { | inline float in2px(float in) { | ||||
return in * SVG_DPI; | |||||
return in * APP_SVG_DPI; | |||||
} | } | ||||
inline math::Vec in2px(math::Vec in) { | inline math::Vec in2px(math::Vec in) { | ||||
return in.mult(SVG_DPI); | |||||
return in.mult(APP_SVG_DPI); | |||||
} | } | ||||
/** Converts millimeter measurements to pixels */ | /** Converts millimeter measurements to pixels */ | ||||
inline float mm2px(float mm) { | inline float mm2px(float mm) { | ||||
return mm * (SVG_DPI / MM_PER_IN); | |||||
return mm * (APP_SVG_DPI / MM_PER_IN); | |||||
} | } | ||||
inline math::Vec mm2px(math::Vec mm) { | inline math::Vec mm2px(math::Vec mm) { | ||||
return mm.mult(SVG_DPI / MM_PER_IN); | |||||
return mm.mult(APP_SVG_DPI / MM_PER_IN); | |||||
} | } | ||||
@@ -346,5 +346,12 @@ inline Vec Vec::clampSafe(Rect bound) const { | |||||
} | } | ||||
/** Useful for debugging Vecs and Rects, e.g. | |||||
printf("%f %f %f %f", RECT_ARGS(r)); | |||||
*/ | |||||
#define VEC_ARGS(v) (v).x, (v).y | |||||
#define RECT_ARGS(r) (r).pos.x, (r).pos.y, (r).size.x, (r).size.y | |||||
} // namespace math | } // namespace math | ||||
} // namespace rack | } // namespace rack |
@@ -12,19 +12,23 @@ Events are not passed to the underlying scene. | |||||
struct FramebufferWidget : Widget { | struct FramebufferWidget : Widget { | ||||
/** Set this to true to re-render the children to the framebuffer the next time it is drawn */ | /** Set this to true to re-render the children to the framebuffer the next time it is drawn */ | ||||
bool dirty = true; | bool dirty = true; | ||||
/** A margin in pixels around the children in the framebuffer | |||||
This prevents cutting the rendered SVG off on the box edges. | |||||
*/ | |||||
float oversample; | float oversample; | ||||
/** The root object in the framebuffer scene | |||||
The FramebufferWidget owns the pointer | |||||
NVGLUframebuffer *fb = NULL; | |||||
/** Pixel dimensions of the allocated framebuffer */ | |||||
math::Vec fbSize; | |||||
/** Bounding box in world coordinates of where the framebuffer should be painted | |||||
Always has integer coordinates so that blitting framebuffers is pixel-perfect. | |||||
*/ | */ | ||||
struct Internal; | |||||
Internal *internal; | |||||
math::Rect fbBox; | |||||
/** Local scale relative to the world scale */ | |||||
math::Vec fbScale; | |||||
/** Subpixel offset of fbBox in world coordinates */ | |||||
math::Vec fbOffset; | |||||
FramebufferWidget(); | FramebufferWidget(); | ||||
~FramebufferWidget(); | ~FramebufferWidget(); | ||||
void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
virtual void drawFramebuffer(NVGcontext *vg); | |||||
int getImageHandle(); | int getImageHandle(); | ||||
void onZoom(const event::Zoom &e) override { | void onZoom(const event::Zoom &e) override { | ||||
@@ -7,6 +7,9 @@ | |||||
#include <GL/glew.h> | #include <GL/glew.h> | ||||
#include <GLFW/glfw3.h> | #include <GLFW/glfw3.h> | ||||
#include <nanovg.h> | #include <nanovg.h> | ||||
#define NANOVG_GL2 | |||||
#include <nanovg_gl.h> | |||||
#include <nanovg_gl_utils.h> | |||||
#include <nanosvg.h> | #include <nanosvg.h> | ||||
@@ -60,7 +63,8 @@ struct SVG { | |||||
struct Window { | struct Window { | ||||
GLFWwindow *win = NULL; | GLFWwindow *win = NULL; | ||||
NVGcontext *vg = NULL; | NVGcontext *vg = NULL; | ||||
NVGcontext *framebufferVg = NULL; | |||||
/** Secondary nanovg context for drawing to framebuffers */ | |||||
NVGcontext *fbVg = NULL; | |||||
/** The scaling ratio */ | /** The scaling ratio */ | ||||
float pixelRatio = 1.f; | float pixelRatio = 1.f; | ||||
/* The ratio between the framebuffer size and the window size reported by the OS. | /* The ratio between the framebuffer size and the window size reported by the OS. | ||||
@@ -181,12 +181,12 @@ void ModuleWidget::onHover(const event::Hover &e) { | |||||
OpaqueWidget::onHover(e); | OpaqueWidget::onHover(e); | ||||
// Instead of checking key-down events, delete the module even if key-repeat hasn't fired yet and the cursor is hovering over the widget. | // Instead of checking key-down events, delete the module even if key-repeat hasn't fired yet and the cursor is hovering over the widget. | ||||
if (glfwGetKey(app()->window->win, GLFW_KEY_DELETE) == GLFW_PRESS || glfwGetKey(app()->window->win, GLFW_KEY_BACKSPACE) == GLFW_PRESS) { | |||||
if ((app()->window->getMods() & WINDOW_MOD_MASK) == WINDOW_MOD_CTRL) { | |||||
removeAction(); | |||||
e.consume(NULL); | |||||
return; | |||||
} | |||||
if ((glfwGetKey(app()->window->win, GLFW_KEY_DELETE) == GLFW_PRESS | |||||
|| glfwGetKey(app()->window->win, GLFW_KEY_BACKSPACE) == GLFW_PRESS) | |||||
&& (app()->window->getMods() & WINDOW_MOD_MASK) == 0) { | |||||
removeAction(); | |||||
e.consume(NULL); | |||||
return; | |||||
} | } | ||||
} | } | ||||
@@ -0,0 +1,39 @@ | |||||
#include "app/SVGPanel.hpp" | |||||
namespace rack { | |||||
void PanelBorder::draw(NVGcontext *vg) { | |||||
NVGcolor borderColor = nvgRGBAf(0.5, 0.5, 0.5, 0.5); | |||||
nvgBeginPath(vg); | |||||
nvgRect(vg, 0.5, 0.5, box.size.x - 1.0, box.size.y - 1.0); | |||||
nvgStrokeColor(vg, borderColor); | |||||
nvgStrokeWidth(vg, 1.0); | |||||
nvgStroke(vg); | |||||
} | |||||
void SVGPanel::step() { | |||||
if (math::isNear(app()->window->pixelRatio, 1.0)) { | |||||
// Small details draw poorly at low DPI, so oversample when drawing to the framebuffer | |||||
oversample = 2.0; | |||||
} | |||||
FramebufferWidget::step(); | |||||
} | |||||
void SVGPanel::setBackground(std::shared_ptr<SVG> svg) { | |||||
SVGWidget *sw = new SVGWidget; | |||||
sw->setSVG(svg); | |||||
addChild(sw); | |||||
// Set size | |||||
box.size = sw->box.size.div(RACK_GRID_SIZE).round().mult(RACK_GRID_SIZE); | |||||
PanelBorder *pb = new PanelBorder; | |||||
pb->box.size = box.size; | |||||
addChild(pb); | |||||
} | |||||
} // namespace rack |
@@ -2,18 +2,18 @@ | |||||
#include "system.hpp" | #include "system.hpp" | ||||
#include "plugin/Plugin.hpp" | #include "plugin/Plugin.hpp" | ||||
#if ARCH_MAC | |||||
#if defined ARCH_MAC | |||||
#include <CoreFoundation/CoreFoundation.h> | #include <CoreFoundation/CoreFoundation.h> | ||||
#include <pwd.h> | #include <pwd.h> | ||||
#endif | #endif | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
#include <Windows.h> | #include <Windows.h> | ||||
#include <Shlobj.h> | #include <Shlobj.h> | ||||
#include <Shlwapi.h> | #include <Shlwapi.h> | ||||
#endif | #endif | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
#include <unistd.h> | #include <unistd.h> | ||||
#include <sys/types.h> | #include <sys/types.h> | ||||
#include <pwd.h> | #include <pwd.h> | ||||
@@ -31,7 +31,7 @@ void init(bool devMode) { | |||||
systemDir = "."; | systemDir = "."; | ||||
} | } | ||||
else { | else { | ||||
#if ARCH_MAC | |||||
#if defined ARCH_MAC | |||||
CFBundleRef bundle = CFBundleGetMainBundle(); | CFBundleRef bundle = CFBundleGetMainBundle(); | ||||
assert(bundle); | assert(bundle); | ||||
CFURLRef resourcesUrl = CFBundleCopyResourcesDirectoryURL(bundle); | CFURLRef resourcesUrl = CFBundleCopyResourcesDirectoryURL(bundle); | ||||
@@ -41,14 +41,14 @@ void init(bool devMode) { | |||||
CFRelease(resourcesUrl); | CFRelease(resourcesUrl); | ||||
systemDir = resourcesBuf; | systemDir = resourcesBuf; | ||||
#endif | #endif | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
char moduleBuf[MAX_PATH]; | char moduleBuf[MAX_PATH]; | ||||
DWORD length = GetModuleFileName(NULL, moduleBuf, sizeof(moduleBuf)); | DWORD length = GetModuleFileName(NULL, moduleBuf, sizeof(moduleBuf)); | ||||
assert(length > 0); | assert(length > 0); | ||||
PathRemoveFileSpec(moduleBuf); | PathRemoveFileSpec(moduleBuf); | ||||
systemDir = moduleBuf; | systemDir = moduleBuf; | ||||
#endif | #endif | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
// TODO For now, users should launch Rack from their terminal in the system directory | // TODO For now, users should launch Rack from their terminal in the system directory | ||||
systemDir = "."; | systemDir = "."; | ||||
#endif | #endif | ||||
@@ -61,7 +61,7 @@ void init(bool devMode) { | |||||
userDir = "."; | userDir = "."; | ||||
} | } | ||||
else { | else { | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
// Get "My Documents" folder | // Get "My Documents" folder | ||||
char documentsBuf[MAX_PATH]; | char documentsBuf[MAX_PATH]; | ||||
HRESULT result = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, documentsBuf); | HRESULT result = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, documentsBuf); | ||||
@@ -69,14 +69,14 @@ void init(bool devMode) { | |||||
userDir = documentsBuf; | userDir = documentsBuf; | ||||
userDir += "/Rack"; | userDir += "/Rack"; | ||||
#endif | #endif | ||||
#if ARCH_MAC | |||||
#if defined ARCH_MAC | |||||
// Get home directory | // Get home directory | ||||
struct passwd *pw = getpwuid(getuid()); | struct passwd *pw = getpwuid(getuid()); | ||||
assert(pw); | assert(pw); | ||||
userDir = pw->pw_dir; | userDir = pw->pw_dir; | ||||
userDir += "/Documents/Rack"; | userDir += "/Documents/Rack"; | ||||
#endif | #endif | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
// Get home directory | // Get home directory | ||||
const char *homeBuf = getenv("HOME"); | const char *homeBuf = getenv("HOME"); | ||||
if (!homeBuf) { | if (!homeBuf) { | ||||
@@ -4,7 +4,7 @@ | |||||
#include "dsp/ringbuffer.hpp" | #include "dsp/ringbuffer.hpp" | ||||
#include <unistd.h> | #include <unistd.h> | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
#include <winsock2.h> | #include <winsock2.h> | ||||
#else | #else | ||||
#include <sys/socket.h> | #include <sys/socket.h> | ||||
@@ -87,7 +87,7 @@ struct BridgeClientConnection { | |||||
if (length <= 0) | if (length <= 0) | ||||
return false; | return false; | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
int flags = MSG_NOSIGNAL; | int flags = MSG_NOSIGNAL; | ||||
#else | #else | ||||
int flags = 0; | int flags = 0; | ||||
@@ -114,7 +114,7 @@ struct BridgeClientConnection { | |||||
if (length <= 0) | if (length <= 0) | ||||
return false; | return false; | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
int flags = MSG_NOSIGNAL; | int flags = MSG_NOSIGNAL; | ||||
#else | #else | ||||
int flags = 0; | int flags = 0; | ||||
@@ -282,7 +282,7 @@ struct BridgeClientConnection { | |||||
static void clientRun(int client) { | static void clientRun(int client) { | ||||
DEFER({ | DEFER({ | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
if (shutdown(client, SD_SEND)) { | if (shutdown(client, SD_SEND)) { | ||||
WARN("Bridge client shutdown() failed"); | WARN("Bridge client shutdown() failed"); | ||||
} | } | ||||
@@ -296,7 +296,7 @@ static void clientRun(int client) { | |||||
#endif | #endif | ||||
}); | }); | ||||
#if ARCH_MAC | |||||
#if defined ARCH_MAC | |||||
// Avoid SIGPIPE | // Avoid SIGPIPE | ||||
int flag = 1; | int flag = 1; | ||||
if (setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &flag, sizeof(int))) { | if (setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &flag, sizeof(int))) { | ||||
@@ -306,7 +306,7 @@ static void clientRun(int client) { | |||||
#endif | #endif | ||||
// Disable non-blocking | // Disable non-blocking | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
unsigned long blockingMode = 0; | unsigned long blockingMode = 0; | ||||
if (ioctlsocket(client, FIONBIO, &blockingMode)) { | if (ioctlsocket(client, FIONBIO, &blockingMode)) { | ||||
WARN("Bridge client ioctlsocket() failed"); | WARN("Bridge client ioctlsocket() failed"); | ||||
@@ -327,7 +327,7 @@ static void clientRun(int client) { | |||||
static void serverConnect() { | static void serverConnect() { | ||||
// Initialize sockets | // Initialize sockets | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
WSADATA wsaData; | WSADATA wsaData; | ||||
if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { | if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { | ||||
WARN("Bridge server WSAStartup() failed"); | WARN("Bridge server WSAStartup() failed"); | ||||
@@ -343,7 +343,7 @@ static void serverConnect() { | |||||
memset(&addr, 0, sizeof(addr)); | memset(&addr, 0, sizeof(addr)); | ||||
addr.sin_family = AF_INET; | addr.sin_family = AF_INET; | ||||
addr.sin_port = htons(BRIDGE_PORT); | addr.sin_port = htons(BRIDGE_PORT); | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
addr.sin_addr.s_addr = inet_addr(BRIDGE_HOST); | addr.sin_addr.s_addr = inet_addr(BRIDGE_HOST); | ||||
#else | #else | ||||
inet_pton(AF_INET, BRIDGE_HOST, &addr.sin_addr); | inet_pton(AF_INET, BRIDGE_HOST, &addr.sin_addr); | ||||
@@ -363,7 +363,7 @@ static void serverConnect() { | |||||
INFO("Bridge server closed"); | INFO("Bridge server closed"); | ||||
}); | }); | ||||
#if ARCH_MAC || ARCH_LIN | |||||
#if defined ARCH_MAC || defined ARCH_LIN | |||||
int reuseAddrFlag = 1; | int reuseAddrFlag = 1; | ||||
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &reuseAddrFlag, sizeof(reuseAddrFlag)); | setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &reuseAddrFlag, sizeof(reuseAddrFlag)); | ||||
#endif | #endif | ||||
@@ -382,7 +382,7 @@ static void serverConnect() { | |||||
INFO("Bridge server started"); | INFO("Bridge server started"); | ||||
// Enable non-blocking | // Enable non-blocking | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
unsigned long blockingMode = 1; | unsigned long blockingMode = 1; | ||||
if (ioctlsocket(server, FIONBIO, &blockingMode)) { | if (ioctlsocket(server, FIONBIO, &blockingMode)) { | ||||
WARN("Bridge server ioctlsocket() failed"); | WARN("Bridge server ioctlsocket() failed"); | ||||
@@ -0,0 +1,18 @@ | |||||
// This source file compiles those annoying implementation-in-header libraries | |||||
#define GLEW_STATIC | |||||
#include <GL/glew.h> | |||||
#include <nanovg.h> | |||||
#define NANOVG_GL2_IMPLEMENTATION | |||||
// #define NANOVG_GL3_IMPLEMENTATION | |||||
// #define NANOVG_GLES2_IMPLEMENTATION | |||||
// #define NANOVG_GLES3_IMPLEMENTATION | |||||
#include <nanovg_gl.h> | |||||
// Hack to get framebuffer objects working on OpenGL 2 (we blindly assume the extension is supported) | |||||
#define NANOVG_FBO_VALID | |||||
#include <nanovg_gl_utils.h> | |||||
#define BLENDISH_IMPLEMENTATION | |||||
#include <blendish.h> | |||||
#define NANOSVG_IMPLEMENTATION | |||||
#define NANOSVG_ALL_COLOR_KEYWORDS | |||||
#include <nanosvg.h> |
@@ -19,7 +19,7 @@ | |||||
#include <zip.h> | #include <zip.h> | ||||
#include <jansson.h> | #include <jansson.h> | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
#include <windows.h> | #include <windows.h> | ||||
#include <direct.h> | #include <direct.h> | ||||
#define mkdir(_dir, _perms) _mkdir(_dir) | #define mkdir(_dir, _perms) _mkdir(_dir) | ||||
@@ -62,9 +62,9 @@ static bool loadPlugin(std::string path) { | |||||
// Load plugin library | // Load plugin library | ||||
std::string libraryFilename; | std::string libraryFilename; | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
libraryFilename = path + "/" + "plugin.so"; | libraryFilename = path + "/" + "plugin.so"; | ||||
#elif ARCH_WIN | |||||
#elif defined ARCH_WIN | |||||
libraryFilename = path + "/" + "plugin.dll"; | libraryFilename = path + "/" + "plugin.dll"; | ||||
#elif ARCH_MAC | #elif ARCH_MAC | ||||
libraryFilename = path + "/" + "plugin.dylib"; | libraryFilename = path + "/" + "plugin.dylib"; | ||||
@@ -77,7 +77,7 @@ static bool loadPlugin(std::string path) { | |||||
} | } | ||||
// Load dynamic/shared library | // Load dynamic/shared library | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); | SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); | ||||
HINSTANCE handle = LoadLibrary(libraryFilename.c_str()); | HINSTANCE handle = LoadLibrary(libraryFilename.c_str()); | ||||
SetErrorMode(0); | SetErrorMode(0); | ||||
@@ -97,7 +97,7 @@ static bool loadPlugin(std::string path) { | |||||
// Call plugin's init() function | // Call plugin's init() function | ||||
typedef void (*InitCallback)(Plugin *); | typedef void (*InitCallback)(Plugin *); | ||||
InitCallback initCallback; | InitCallback initCallback; | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
initCallback = (InitCallback) GetProcAddress(handle, "init"); | initCallback = (InitCallback) GetProcAddress(handle, "init"); | ||||
#else | #else | ||||
initCallback = (InitCallback) dlsym(handle, "init"); | initCallback = (InitCallback) dlsym(handle, "init"); | ||||
@@ -170,11 +170,11 @@ static bool syncPlugin(std::string slug, json_t *manifestJ, bool dryRun) { | |||||
name = slug; | name = slug; | ||||
} | } | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
std::string arch = "win"; | std::string arch = "win"; | ||||
#elif ARCH_MAC | #elif ARCH_MAC | ||||
std::string arch = "mac"; | std::string arch = "mac"; | ||||
#elif ARCH_LIN | |||||
#elif defined ARCH_LIN | |||||
std::string arch = "lin"; | std::string arch = "lin"; | ||||
#endif | #endif | ||||
@@ -359,7 +359,7 @@ void init(bool devMode) { | |||||
void destroy() { | void destroy() { | ||||
for (Plugin *plugin : plugins) { | for (Plugin *plugin : plugins) { | ||||
// Free library handle | // Free library handle | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
if (plugin->handle) | if (plugin->handle) | ||||
FreeLibrary((HINSTANCE) plugin->handle); | FreeLibrary((HINSTANCE) plugin->handle); | ||||
#else | #else | ||||
@@ -2,7 +2,7 @@ | |||||
#include <dirent.h> | #include <dirent.h> | ||||
#include <sys/stat.h> | #include <sys/stat.h> | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
#include <windows.h> | #include <windows.h> | ||||
#include <shellapi.h> | #include <shellapi.h> | ||||
#endif | #endif | ||||
@@ -70,7 +70,7 @@ void copyFile(const std::string &srcPath, const std::string &destPath) { | |||||
} | } | ||||
void createDirectory(const std::string &path) { | void createDirectory(const std::string &path) { | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
CreateDirectory(path.c_str(), NULL); | CreateDirectory(path.c_str(), NULL); | ||||
#else | #else | ||||
mkdir(path.c_str(), 0755); | mkdir(path.c_str(), 0755); | ||||
@@ -78,15 +78,15 @@ void createDirectory(const std::string &path) { | |||||
} | } | ||||
void openBrowser(const std::string &url) { | void openBrowser(const std::string &url) { | ||||
#if ARCH_LIN | |||||
#if defined ARCH_LIN | |||||
std::string command = "xdg-open " + url; | std::string command = "xdg-open " + url; | ||||
(void) std::system(command.c_str()); | (void) std::system(command.c_str()); | ||||
#endif | #endif | ||||
#if ARCH_MAC | |||||
#if defined ARCH_MAC | |||||
std::string command = "open " + url; | std::string command = "open " + url; | ||||
std::system(command.c_str()); | std::system(command.c_str()); | ||||
#endif | #endif | ||||
#if ARCH_WIN | |||||
#if defined ARCH_WIN | |||||
ShellExecute(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL); | ShellExecute(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL); | ||||
#endif | #endif | ||||
} | } | ||||
@@ -73,7 +73,12 @@ void ScrollWidget::onHover(const event::Hover &e) { | |||||
} | } | ||||
void ScrollWidget::onHoverScroll(const event::HoverScroll &e) { | void ScrollWidget::onHoverScroll(const event::HoverScroll &e) { | ||||
offset = offset.minus(e.scrollDelta); | |||||
math::Vec scrollDelta = e.scrollDelta; | |||||
// Flip coordinates if shift is held | |||||
if ((app()->window->getMods() & WINDOW_MOD_MASK) == GLFW_MOD_SHIFT) | |||||
scrollDelta = scrollDelta.flip(); | |||||
offset = offset.minus(scrollDelta); | |||||
e.consume(this); | e.consume(this); | ||||
} | } | ||||
@@ -1,34 +1,17 @@ | |||||
#include "widgets/FramebufferWidget.hpp" | #include "widgets/FramebufferWidget.hpp" | ||||
#include "app.hpp" | #include "app.hpp" | ||||
#include <nanovg_gl.h> | |||||
#include <nanovg_gl_utils.h> | |||||
namespace rack { | namespace rack { | ||||
struct FramebufferWidget::Internal { | |||||
NVGLUframebuffer *fb = NULL; | |||||
math::Rect box; | |||||
~Internal() { | |||||
setFramebuffer(NULL); | |||||
} | |||||
void setFramebuffer(NVGLUframebuffer *fb) { | |||||
if (this->fb) | |||||
nvgluDeleteFramebuffer(this->fb); | |||||
this->fb = fb; | |||||
} | |||||
}; | |||||
FramebufferWidget::FramebufferWidget() { | FramebufferWidget::FramebufferWidget() { | ||||
oversample = 1.0; | oversample = 1.0; | ||||
internal = new Internal; | |||||
} | } | ||||
FramebufferWidget::~FramebufferWidget() { | FramebufferWidget::~FramebufferWidget() { | ||||
delete internal; | |||||
if (fb) | |||||
nvgluDeleteFramebuffer(fb); | |||||
} | } | ||||
void FramebufferWidget::draw(NVGcontext *vg) { | void FramebufferWidget::draw(NVGcontext *vg) { | ||||
@@ -40,83 +23,106 @@ void FramebufferWidget::draw(NVGcontext *vg) { | |||||
float xform[6]; | float xform[6]; | ||||
nvgCurrentTransform(vg, xform); | nvgCurrentTransform(vg, xform); | ||||
// Skew and rotate is not supported | // Skew and rotate is not supported | ||||
assert(std::abs(xform[1]) < 1e-6); | |||||
assert(std::abs(xform[2]) < 1e-6); | |||||
math::Vec s = math::Vec(xform[0], xform[3]); | |||||
math::Vec b = math::Vec(xform[4], xform[5]); | |||||
math::Vec bi = b.floor(); | |||||
math::Vec bf = b.minus(bi); | |||||
assert(math::isNear(xform[1], 0.f)); | |||||
assert(math::isNear(xform[2], 0.f)); | |||||
// Extract scale and offset from world transform | |||||
math::Vec scale = math::Vec(xform[0], xform[3]); | |||||
math::Vec offset = math::Vec(xform[4], xform[5]); | |||||
math::Vec offsetI = offset.floor(); | |||||
// Render to framebuffer | // Render to framebuffer | ||||
if (dirty) { | if (dirty) { | ||||
dirty = false; | dirty = false; | ||||
internal->box = getChildrenBoundingBox(); | |||||
internal->box.pos = internal->box.pos.mult(s).floor(); | |||||
internal->box.size = internal->box.size.mult(s).ceil().plus(math::Vec(1, 1)); | |||||
math::Vec fbSize = internal->box.size.mult(app()->window->pixelRatio * oversample); | |||||
if (!fbSize.isFinite()) | |||||
return; | |||||
if (fbSize.isZero()) | |||||
return; | |||||
fbScale = scale; | |||||
// World coordinates, in range [0, 1) | |||||
fbOffset = offset.minus(offsetI); | |||||
math::Rect localBox; | |||||
if (children.empty()) { | |||||
localBox = box.zeroPos(); | |||||
} | |||||
else { | |||||
localBox = getChildrenBoundingBox(); | |||||
} | |||||
// DEBUG("%g %g %g %g, %g %g, %g %g", RECT_ARGS(localBox), VEC_ARGS(fbOffset), VEC_ARGS(scale)); | |||||
// Transform to world coordinates, then expand to nearest integer coordinates | |||||
math::Vec min = localBox.getTopLeft().mult(scale).plus(fbOffset).floor(); | |||||
math::Vec max = localBox.getBottomRight().mult(scale).plus(fbOffset).ceil(); | |||||
fbBox = math::Rect::fromMinMax(min, max); | |||||
// DEBUG("%g %g %g %g", RECT_ARGS(fbBox)); | |||||
math::Vec newFbSize = fbBox.size.mult(app()->window->pixelRatio * oversample); | |||||
if (!fb || !newFbSize.isEqual(fbSize)) { | |||||
fbSize = newFbSize; | |||||
// Delete old framebuffer | |||||
if (fb) | |||||
nvgluDeleteFramebuffer(fb); | |||||
// Create a framebuffer from the main nanovg context. We will draw to this in the secondary nanovg context. | |||||
if (fbSize.isFinite() && !fbSize.isZero()) | |||||
fb = nvgluCreateFramebuffer(vg, fbSize.x, fbSize.y, 0); | |||||
} | |||||
// INFO("rendering framebuffer %f %f", fbSize.x, fbSize.y); | |||||
// Delete old one first to free up GPU memory | |||||
internal->setFramebuffer(NULL); | |||||
// Create a framebuffer from the main nanovg context. We will draw to this in the secondary nanovg context. | |||||
NVGLUframebuffer *fb = nvgluCreateFramebuffer(app()->window->vg, fbSize.x, fbSize.y, 0); | |||||
if (!fb) | if (!fb) | ||||
return; | return; | ||||
internal->setFramebuffer(fb); | |||||
nvgluBindFramebuffer(fb); | nvgluBindFramebuffer(fb); | ||||
glViewport(0.0, 0.0, fbSize.x, fbSize.y); | |||||
glClearColor(0.0, 0.0, 0.0, 0.0); | |||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||||
NVGcontext *framebufferVg = app()->window->framebufferVg; | |||||
nvgBeginFrame(framebufferVg, fbSize.x, fbSize.y, app()->window->pixelRatio * oversample); | |||||
nvgScale(framebufferVg, app()->window->pixelRatio * oversample, app()->window->pixelRatio * oversample); | |||||
// Use local scaling | |||||
nvgTranslate(framebufferVg, bf.x, bf.y); | |||||
nvgTranslate(framebufferVg, -internal->box.pos.x, -internal->box.pos.y); | |||||
nvgScale(framebufferVg, s.x, s.y); | |||||
Widget::draw(framebufferVg); | |||||
nvgEndFrame(framebufferVg); | |||||
drawFramebuffer(app()->window->fbVg); | |||||
nvgluBindFramebuffer(NULL); | nvgluBindFramebuffer(NULL); | ||||
} | } | ||||
if (!internal->fb) { | |||||
if (!fb) | |||||
return; | return; | ||||
} | |||||
// Draw framebuffer image, using world coordinates | // Draw framebuffer image, using world coordinates | ||||
nvgSave(vg); | nvgSave(vg); | ||||
nvgResetTransform(vg); | nvgResetTransform(vg); | ||||
nvgTranslate(vg, bi.x, bi.y); | |||||
nvgBeginPath(vg); | nvgBeginPath(vg); | ||||
nvgRect(vg, internal->box.pos.x, internal->box.pos.y, internal->box.size.x, internal->box.size.y); | |||||
NVGpaint paint = nvgImagePattern(vg, internal->box.pos.x, internal->box.pos.y, internal->box.size.x, internal->box.size.y, 0.0, internal->fb->image, 1.0); | |||||
nvgRect(vg, | |||||
offsetI.x + fbBox.pos.x, | |||||
offsetI.y + fbBox.pos.y, | |||||
fbBox.size.x, fbBox.size.y); | |||||
NVGpaint paint = nvgImagePattern(vg, | |||||
offsetI.x + fbBox.pos.x, | |||||
offsetI.y + fbBox.pos.y, | |||||
fbBox.size.x, fbBox.size.y, | |||||
0.0, fb->image, 1.0); | |||||
nvgFillPaint(vg, paint); | nvgFillPaint(vg, paint); | ||||
nvgFill(vg); | nvgFill(vg); | ||||
// For debugging the bounding box of the framebuffer | // For debugging the bounding box of the framebuffer | ||||
// nvgStrokeWidth(vg, 2.0); | // nvgStrokeWidth(vg, 2.0); | ||||
// nvgStrokeColor(vg, nvgRGBA(255, 0, 0, 128)); | |||||
// nvgStrokeColor(vg, nvgRGBAf(1, 1, 0, 0.5)); | |||||
// nvgStroke(vg); | // nvgStroke(vg); | ||||
nvgRestore(vg); | nvgRestore(vg); | ||||
} | } | ||||
void FramebufferWidget::drawFramebuffer(NVGcontext *vg) { | |||||
float pixelRatio = fbSize.x / fbBox.size.x; | |||||
nvgBeginFrame(vg, fbBox.size.x, fbBox.size.y, pixelRatio); | |||||
// Use local scaling | |||||
nvgTranslate(vg, -fbBox.pos.x, -fbBox.pos.y); | |||||
nvgTranslate(vg, fbOffset.x, fbOffset.y); | |||||
nvgScale(vg, fbScale.x, fbScale.y); | |||||
Widget::draw(vg); | |||||
glViewport(0.0, 0.0, fbSize.x, fbSize.y); | |||||
glClearColor(0.0, 0.0, 0.0, 0.0); | |||||
// glClearColor(0.0, 1.0, 1.0, 0.5); | |||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||||
nvgEndFrame(vg); | |||||
} | |||||
int FramebufferWidget::getImageHandle() { | int FramebufferWidget::getImageHandle() { | ||||
if (!internal->fb) | |||||
if (!fb) | |||||
return -1; | return -1; | ||||
return internal->fb->image; | |||||
return fb->image; | |||||
} | } | ||||
@@ -14,16 +14,13 @@ Widget::~Widget() { | |||||
} | } | ||||
math::Rect Widget::getChildrenBoundingBox() { | math::Rect Widget::getChildrenBoundingBox() { | ||||
math::Rect bound; | |||||
math::Vec min = math::Vec(INFINITY, INFINITY); | |||||
math::Vec max = math::Vec(-INFINITY, -INFINITY); | |||||
for (Widget *child : children) { | for (Widget *child : children) { | ||||
if (child == children.front()) { | |||||
bound = child->box; | |||||
} | |||||
else { | |||||
bound = bound.expand(child->box); | |||||
} | |||||
min = min.min(child->box.getTopLeft()); | |||||
max = max.max(child->box.getBottomRight()); | |||||
} | } | ||||
return bound; | |||||
return math::Rect::fromMinMax(min, max); | |||||
} | } | ||||
math::Vec Widget::getRelativeOffset(math::Vec v, Widget *relative) { | math::Vec Widget::getRelativeOffset(math::Vec v, Widget *relative) { | ||||
@@ -17,20 +17,6 @@ | |||||
#include <osdialog.h> | #include <osdialog.h> | ||||
#define NANOVG_GL2_IMPLEMENTATION 1 | |||||
// #define NANOVG_GL3_IMPLEMENTATION 1 | |||||
// #define NANOVG_GLES2_IMPLEMENTATION 1 | |||||
// #define NANOVG_GLES3_IMPLEMENTATION 1 | |||||
#include <nanovg_gl.h> | |||||
// Hack to get framebuffer objects working on OpenGL 2 (we blindly assume the extension is supported) | |||||
#define NANOVG_FBO_VALID 1 | |||||
#include <nanovg_gl_utils.h> | |||||
#define BLENDISH_IMPLEMENTATION | |||||
#include <blendish.h> | |||||
#define NANOSVG_IMPLEMENTATION | |||||
#define NANOSVG_ALL_COLOR_KEYWORDS | |||||
#include <nanosvg.h> | |||||
namespace rack { | namespace rack { | ||||
@@ -84,7 +70,7 @@ std::shared_ptr<Image> Image::load(const std::string &filename) { | |||||
} | } | ||||
SVG::SVG(const std::string &filename) { | SVG::SVG(const std::string &filename) { | ||||
handle = nsvgParseFromFile(filename.c_str(), "px", SVG_DPI); | |||||
handle = nsvgParseFromFile(filename.c_str(), "px", APP_SVG_DPI); | |||||
if (handle) { | if (handle) { | ||||
INFO("Loaded SVG %s", filename.c_str()); | INFO("Loaded SVG %s", filename.c_str()); | ||||
} | } | ||||
@@ -171,10 +157,6 @@ static void scrollCallback(GLFWwindow *win, double x, double y) { | |||||
math::Vec scrollDelta = math::Vec(x, y); | math::Vec scrollDelta = math::Vec(x, y); | ||||
scrollDelta = scrollDelta.mult(50.0); | scrollDelta = scrollDelta.mult(50.0); | ||||
// Flip coordinates if shift is held | |||||
if ((window->getMods() & WINDOW_MOD_MASK) == GLFW_MOD_SHIFT) | |||||
scrollDelta = scrollDelta.flip(); | |||||
app()->event->handleScroll(window->mousePos, scrollDelta); | app()->event->handleScroll(window->mousePos, scrollDelta); | ||||
} | } | ||||
@@ -215,10 +197,10 @@ Window::Window() { | |||||
internal = new Internal; | internal = new Internal; | ||||
int err; | int err; | ||||
#if NANOVG_GL2 | |||||
#if defined NANOVG_GL2 | |||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); | ||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); | ||||
#elif NANOVG_GL3 | |||||
#elif defined NANOVG_GL3 | |||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | ||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); | ||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | ||||
@@ -275,13 +257,13 @@ Window::Window() { | |||||
assert(vg); | assert(vg); | ||||
#if defined NANOVG_GL2 | #if defined NANOVG_GL2 | ||||
framebufferVg = nvgCreateGL2(nvgFlags); | |||||
fbVg = nvgCreateGL2(nvgFlags); | |||||
#elif defined NANOVG_GL3 | #elif defined NANOVG_GL3 | ||||
framebufferVg = nvgCreateGL3(nvgFlags); | |||||
fbVg = nvgCreateGL3(nvgFlags); | |||||
#elif defined NANOVG_GLES2 | #elif defined NANOVG_GLES2 | ||||
framebufferVg = nvgCreateGLES2(nvgFlags); | |||||
fbVg = nvgCreateGLES2(nvgFlags); | |||||
#endif | #endif | ||||
assert(framebufferVg); | |||||
assert(fbVg); | |||||
} | } | ||||
Window::~Window() { | Window::~Window() { | ||||
@@ -294,11 +276,11 @@ Window::~Window() { | |||||
#endif | #endif | ||||
#if defined NANOVG_GL2 | #if defined NANOVG_GL2 | ||||
nvgDeleteGL2(framebufferVg); | |||||
nvgDeleteGL2(fbVg); | |||||
#elif defined NANOVG_GL3 | #elif defined NANOVG_GL3 | ||||
nvgDeleteGL3(framebufferVg); | |||||
nvgDeleteGL3(fbVg); | |||||
#elif defined NANOVG_GLES2 | #elif defined NANOVG_GLES2 | ||||
nvgDeleteGLES2(framebufferVg); | |||||
nvgDeleteGLES2(fbVg); | |||||
#endif | #endif | ||||
glfwDestroyWindow(win); | glfwDestroyWindow(win); | ||||
@@ -311,10 +293,11 @@ void Window::run() { | |||||
frame = 0; | frame = 0; | ||||
while(!glfwWindowShouldClose(win)) { | while(!glfwWindowShouldClose(win)) { | ||||
double startTime = glfwGetTime(); | double startTime = glfwGetTime(); | ||||
frame++; | |||||
// Poll events | // Poll events | ||||
glfwPollEvents(); | glfwPollEvents(); | ||||
// In case glfwPollEvents() set another OpenGL context | |||||
glfwMakeContextCurrent(win); | |||||
// Call cursorPosCallback every frame, not just when the mouse moves | // Call cursorPosCallback every frame, not just when the mouse moves | ||||
{ | { | ||||
double xpos, ypos; | double xpos, ypos; | ||||
@@ -364,12 +347,6 @@ void Window::run() { | |||||
// Render | // Render | ||||
bool visible = glfwGetWindowAttrib(win, GLFW_VISIBLE) && !glfwGetWindowAttrib(win, GLFW_ICONIFIED); | bool visible = glfwGetWindowAttrib(win, GLFW_VISIBLE) && !glfwGetWindowAttrib(win, GLFW_ICONIFIED); | ||||
if (visible) { | if (visible) { | ||||
// In case glfwPollEvents() worked with another OpenGL context | |||||
glfwMakeContextCurrent(win); | |||||
glViewport(0, 0, fbWidth, fbHeight); | |||||
glClearColor(0.0, 0.0, 0.0, 1.0); | |||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||||
// Update and render | // Update and render | ||||
nvgBeginFrame(vg, winWidth, winHeight, pixelRatio); | nvgBeginFrame(vg, winWidth, winHeight, pixelRatio); | ||||
@@ -378,7 +355,11 @@ void Window::run() { | |||||
app()->event->rootWidget->draw(vg); | app()->event->rootWidget->draw(vg); | ||||
glViewport(0, 0, fbWidth, fbHeight); | |||||
glClearColor(0.0, 0.0, 0.0, 1.0); | |||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); | |||||
nvgEndFrame(vg); | nvgEndFrame(vg); | ||||
glfwSwapBuffers(win); | glfwSwapBuffers(win); | ||||
} | } | ||||
@@ -391,6 +372,7 @@ void Window::run() { | |||||
} | } | ||||
endTime = glfwGetTime(); | endTime = glfwGetTime(); | ||||
// INFO("%lf fps", 1.0 / (endTime - startTime)); | // INFO("%lf fps", 1.0 / (endTime - startTime)); | ||||
frame++; | |||||
} | } | ||||
} | } | ||||