From f5e8ba0369685adc61afc9354d3606b877e758ce Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Tue, 24 Oct 2017 07:09:09 -0400 Subject: [PATCH 1/3] Use another nanovg context for rendering to the framebuffer --- include/gui.hpp | 1 + include/widgets.hpp | 1 - src/app/RackScene.cpp | 2 +- src/app/RackWidget.cpp | 3 ++ src/gui.cpp | 11 +++++-- src/widgets/FramebufferWidget.cpp | 50 ++++++++++++++----------------- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/include/gui.hpp b/include/gui.hpp index 0fadecb1..de41840c 100644 --- a/include/gui.hpp +++ b/include/gui.hpp @@ -17,6 +17,7 @@ namespace rack { extern GLFWwindow *gWindow; extern NVGcontext *gVg; +extern NVGcontext *gFramebufferVg; extern std::shared_ptr gGuiFont; extern float gPixelRatio; extern bool gAllowCursorLock; diff --git a/include/widgets.hpp b/include/widgets.hpp index cdf56b9b..ea020cab 100644 --- a/include/widgets.hpp +++ b/include/widgets.hpp @@ -228,7 +228,6 @@ struct FramebufferWidget : virtual Widget { FramebufferWidget(); ~FramebufferWidget(); - void step() override; void draw(NVGcontext *vg) override; int getImageHandle(); }; diff --git a/src/app/RackScene.cpp b/src/app/RackScene.cpp index e13e3cea..fa16827a 100644 --- a/src/app/RackScene.cpp +++ b/src/app/RackScene.cpp @@ -31,7 +31,7 @@ RackScene::RackScene() { scrollWidget = new RackScrollWidget(); { zoomWidget = new ZoomWidget(); - zoomWidget->zoom = 1.0; + zoomWidget->zoom = 0.5; { assert(!gRackWidget); gRackWidget = new RackWidget(); diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index 1d32c3e3..d237d145 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -404,6 +404,9 @@ struct AddManufacturerMenuItem : MenuItem { for (Model *model : models) { AddModuleMenuItem *item = new AddModuleMenuItem(); item->text = model->name; + // item->rightText = model->plugin->slug; + // if (!model->plugin->version.empty()) + // item->rightText += " v" + model->plugin->version; item->model = model; item->modulePos = modulePos; menu->pushChild(item); diff --git a/src/gui.cpp b/src/gui.cpp index eb789cef..d7a08a59 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -29,6 +29,7 @@ namespace rack { GLFWwindow *gWindow = NULL; NVGcontext *gVg = NULL; +NVGcontext *gFramebufferVg = NULL; std::shared_ptr gGuiFont; float gPixelRatio = 0.0; bool gAllowCursorLock = true; @@ -220,15 +221,15 @@ void renderGui() { gPixelRatio = (float)width / windowWidth; // Update and render - glViewport(0, 0, width, height); - glClearColor(0.0, 0.0, 0.0, 1.0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); nvgBeginFrame(gVg, width, height, gPixelRatio); nvgReset(gVg); nvgScale(gVg, gPixelRatio, gPixelRatio); gScene->draw(gVg); + glViewport(0, 0, width, height); + glClearColor(0.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); nvgEndFrame(gVg); glfwSwapBuffers(gWindow); } @@ -288,6 +289,9 @@ void guiInit() { // gVg = nvgCreateGL3(NVG_ANTIALIAS); assert(gVg); + gFramebufferVg = nvgCreateGL2(NVG_ANTIALIAS); + assert(gFramebufferVg); + // Set up Blendish gGuiFont = Font::load(assetGlobal("res/DejaVuSans.ttf")); bndSetFont(gGuiFont->handle); @@ -298,6 +302,7 @@ void guiDestroy() { gGuiFont.reset(); nvgDeleteGL2(gVg); // nvgDeleteGL3(gVg); + nvgDeleteGL2(gFramebufferVg); glfwDestroyWindow(gWindow); glfwTerminate(); } diff --git a/src/widgets/FramebufferWidget.cpp b/src/widgets/FramebufferWidget.cpp index 21a14158..bb95ca00 100644 --- a/src/widgets/FramebufferWidget.cpp +++ b/src/widgets/FramebufferWidget.cpp @@ -32,11 +32,7 @@ FramebufferWidget::~FramebufferWidget() { delete internal; } -void FramebufferWidget::step() { - // Step children before rendering - Widget::step(); - - // Render the scene to the framebuffer if dirty +void FramebufferWidget::draw(NVGcontext *vg) { if (dirty) { internal->box.pos = Vec(0, 0); internal->box.size = box.size; @@ -50,6 +46,7 @@ void FramebufferWidget::step() { // 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(gVg, fbSize.x, fbSize.y, NVG_IMAGE_REPEATX | NVG_IMAGE_REPEATY); if (!fb) return; @@ -59,36 +56,19 @@ void FramebufferWidget::step() { 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); - nvgBeginFrame(gVg, fbSize.x, fbSize.y, gPixelRatio * oversample); - nvgScale(gVg, gPixelRatio * oversample, gPixelRatio * oversample); - Widget::draw(gVg); + nvgBeginFrame(gFramebufferVg, fbSize.x, fbSize.y, gPixelRatio * oversample); - nvgEndFrame(gVg); + nvgScale(gFramebufferVg, gPixelRatio * oversample, gPixelRatio * oversample); + Widget::draw(gFramebufferVg); + + nvgEndFrame(gFramebufferVg); nvgluBindFramebuffer(NULL); dirty = false; } -} - -void FramebufferWidget::draw(NVGcontext *vg) { - // { - // float xform[6]; - // nvgCurrentTransform(vg, xform); - // printf("%f %f %f %f %f %f\n", xform[0], xform[1], xform[2], xform[3], xform[4], xform[5]); - // nvgSave(vg); - // nvgResetTransform(vg); - // nvgTranslate(vg, xform[5], xform[6]); - // nvgBeginPath(vg); - // nvgRect(vg, 0, 0, 50, 50); - // nvgFillColor(vg, nvgRGBf(1.0, 0.0, 0.0)); - // nvgFill(vg); - // nvgRestore(vg); - // } if (!internal->fb) { - // Bypass framebuffer cache entirely - // Widget::draw(vg); return; } @@ -102,6 +82,22 @@ void FramebufferWidget::draw(NVGcontext *vg) { // For debugging bounding box of framebuffer image // nvgFillColor(vg, nvgRGBA(255, 0, 0, 64)); // nvgFill(vg); + + { + float xform[6]; + nvgCurrentTransform(vg, xform); + // printf("%f %f %f %f; %f %f\n", xform[0], xform[1], xform[2], xform[3], xform[4], xform[5]); + nvgSave(vg); + nvgResetTransform(vg); + nvgTranslate(vg, xform[4], xform[5]); + nvgScale(vg, xform[0], xform[3]); + nvgBeginPath(vg); + nvgRect(vg, 0, 0, internal->box.size.x, internal->box.size.y); + nvgStrokeWidth(vg, 2.0); + nvgStrokeColor(vg, nvgRGBf(1.0, 0.0, 0.0)); + nvgStroke(vg); + nvgRestore(vg); + } } int FramebufferWidget::getImageHandle() { From a22aac6493c27576886550e456490798c3ad1179 Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Tue, 24 Oct 2017 08:47:14 -0400 Subject: [PATCH 2/3] Added zoom slider, zoom to settings, finished Framebuffer scaling --- include/app.hpp | 1 + include/gui.hpp | 2 + include/math.hpp | 6 +++ include/widgets.hpp | 7 +--- src/app/RackScene.cpp | 4 +- src/app/RackScrollWidget.cpp | 1 + src/app/RackWidget.cpp | 6 +-- src/app/Toolbar.cpp | 15 ++++++- src/app/WireWidget.cpp | 1 + src/gui.cpp | 2 + src/settings.cpp | 10 +++++ src/widgets.cpp | 2 - src/widgets/FramebufferWidget.cpp | 66 ++++++++++++++++++++----------- src/widgets/Scene.cpp | 1 + src/widgets/Slider.cpp | 6 +++ src/widgets/TextField.cpp | 4 ++ src/widgets/Tooltip.cpp | 1 + 17 files changed, 100 insertions(+), 35 deletions(-) diff --git a/include/app.hpp b/include/app.hpp index 21a3bdc0..69124234 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -312,6 +312,7 @@ struct SVGScrew : FramebufferWidget { struct Toolbar : OpaqueWidget { Slider *wireOpacitySlider; Slider *wireTensionSlider; + Slider *zoomSlider; RadioButton *cpuUsageButton; RadioButton *plugLightButton; diff --git a/include/gui.hpp b/include/gui.hpp index de41840c..920f6cba 100644 --- a/include/gui.hpp +++ b/include/gui.hpp @@ -21,6 +21,8 @@ extern NVGcontext *gFramebufferVg; extern std::shared_ptr gGuiFont; extern float gPixelRatio; extern bool gAllowCursorLock; +extern int gGuiFrame; +extern Vec gMousePos; void guiInit(); diff --git a/include/math.hpp b/include/math.hpp index af8b67b4..2f38a6ca 100644 --- a/include/math.hpp +++ b/include/math.hpp @@ -191,6 +191,12 @@ struct Vec { Vec round() { return Vec(roundf(x), roundf(y)); } + Vec floor() { + return Vec(floorf(x), floorf(y)); + } + Vec ceil() { + return Vec(ceilf(x), ceilf(y)); + } bool isZero() { return x == 0.0 && y == 0.0; } diff --git a/include/widgets.hpp b/include/widgets.hpp index ea020cab..3d2a42e3 100644 --- a/include/widgets.hpp +++ b/include/widgets.hpp @@ -216,10 +216,6 @@ Events are not passed to the underlying scene. struct FramebufferWidget : virtual Widget { /** Set this to true to re-render the children to the framebuffer in the next step() override */ 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 = 2.0; /** The root object in the framebuffer scene The FramebufferWidget owns the pointer */ @@ -353,6 +349,7 @@ struct Slider : OpaqueWidget, QuantityWidget { void onDragStart() override; void onDragMove(Vec mouseRel) override; void onDragEnd() override; + void onMouseDownOpaque(int button) override; }; /** Parent must be a ScrollWidget */ @@ -438,12 +435,10 @@ struct Scene : OpaqueWidget { // globals //////////////////// -extern Vec gMousePos; extern Widget *gHoveredWidget; extern Widget *gDraggedWidget; extern Widget *gDragHoveredWidget; extern Widget *gFocusedWidget; -extern int gGuiFrame; extern Scene *gScene; diff --git a/src/app/RackScene.cpp b/src/app/RackScene.cpp index fa16827a..b0b6d8ca 100644 --- a/src/app/RackScene.cpp +++ b/src/app/RackScene.cpp @@ -31,7 +31,6 @@ RackScene::RackScene() { scrollWidget = new RackScrollWidget(); { zoomWidget = new ZoomWidget(); - zoomWidget->zoom = 0.5; { assert(!gRackWidget); gRackWidget = new RackWidget(); @@ -63,6 +62,9 @@ void RackScene::step() { .plus(Vec(500, 500)) .div(zoomWidget->zoom); + // Set zoom from the toolbar's zoom slider + zoomWidget->zoom = gToolbar->zoomSlider->value / 100.0; + Scene::step(); zoomWidget->box.size = gRackWidget->box.size.mult(zoomWidget->zoom); diff --git a/src/app/RackScrollWidget.cpp b/src/app/RackScrollWidget.cpp index b4bafafd..b2d4357f 100644 --- a/src/app/RackScrollWidget.cpp +++ b/src/app/RackScrollWidget.cpp @@ -1,4 +1,5 @@ #include "app.hpp" +#include "gui.hpp" namespace rack { diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index d237d145..ad35d45f 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -189,9 +189,9 @@ void RackWidget::fromJson(json_t *rootJ) { // version json_t *versionJ = json_object_get(rootJ, "version"); if (versionJ) { - const char *version = json_string_value(versionJ); - if (gApplicationVersion != version) - message += stringf("This patch was created with Rack %s. Saving it will convert it to a Rack %s patch.\n\n", version, gApplicationVersion.c_str()); + std::string version = json_string_value(versionJ); + if (!version.empty() && gApplicationVersion != version) + message += stringf("This patch was created with Rack %s. Saving it will convert it to a Rack %s patch.\n\n", version.c_str(), gApplicationVersion.empty() ? "dev" : gApplicationVersion.c_str()); } // modules diff --git a/src/app/Toolbar.cpp b/src/app/Toolbar.cpp index d0def913..df526bc3 100644 --- a/src/app/Toolbar.cpp +++ b/src/app/Toolbar.cpp @@ -139,13 +139,26 @@ Toolbar::Toolbar() { wireTensionSlider->box.pos = Vec(xPos, margin); wireTensionSlider->box.size.x = 150; wireTensionSlider->label = "Cable tension"; - // wireTensionSlider->unit = ""; + wireTensionSlider->unit = ""; wireTensionSlider->setLimits(0.0, 1.0); wireTensionSlider->setDefaultValue(0.5); addChild(wireTensionSlider); xPos += wireTensionSlider->box.size.x; } + xPos += margin; + { + zoomSlider = new Slider(); + zoomSlider->box.pos = Vec(xPos, margin); + zoomSlider->box.size.x = 150; + zoomSlider->label = "Zoom"; + zoomSlider->unit = "%"; + zoomSlider->setLimits(50.0, 200.0); + zoomSlider->setDefaultValue(100.0); + addChild(zoomSlider); + xPos += zoomSlider->box.size.x; + } + xPos += margin; { plugLightButton = new RadioButton(); diff --git a/src/app/WireWidget.cpp b/src/app/WireWidget.cpp index 9267e79f..21dcf940 100644 --- a/src/app/WireWidget.cpp +++ b/src/app/WireWidget.cpp @@ -1,6 +1,7 @@ #include "app.hpp" #include "engine.hpp" #include "components.hpp" +#include "gui.hpp" namespace rack { diff --git a/src/gui.cpp b/src/gui.cpp index d7a08a59..e8c260c5 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -33,6 +33,8 @@ NVGcontext *gFramebufferVg = NULL; std::shared_ptr gGuiFont; float gPixelRatio = 0.0; bool gAllowCursorLock = true; +int gGuiFrame; +Vec gMousePos; void windowSizeCallback(GLFWwindow* window, int width, int height) { diff --git a/src/settings.cpp b/src/settings.cpp index bbfd2ec1..d90faa0d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -27,6 +27,11 @@ static json_t *settingsToJson() { json_t *tensionJ = json_real(tension); json_object_set_new(rootJ, "wireTension", tensionJ); + // zoom + float zoom = gToolbar->zoomSlider->value; + json_t *zoomJ = json_real(zoom); + json_object_set_new(rootJ, "zoom", zoomJ); + // allowCursorLock json_t *allowCursorLockJ = json_boolean(gAllowCursorLock); json_object_set_new(rootJ, "allowCursorLock", allowCursorLockJ); @@ -62,6 +67,11 @@ static void settingsFromJson(json_t *rootJ) { if (tensionJ) gToolbar->wireTensionSlider->value = json_number_value(tensionJ); + // zoom + json_t *zoomJ = json_object_get(rootJ, "zoom"); + if (zoomJ) + gToolbar->zoomSlider->value = json_number_value(zoomJ); + // allowCursorLock json_t *allowCursorLockJ = json_object_get(rootJ, "allowCursorLock"); if (allowCursorLockJ) diff --git a/src/widgets.cpp b/src/widgets.cpp index fa807bc0..ff6c5c0b 100644 --- a/src/widgets.cpp +++ b/src/widgets.cpp @@ -2,12 +2,10 @@ namespace rack { -Vec gMousePos; Widget *gHoveredWidget = NULL; Widget *gDraggedWidget = NULL; Widget *gDragHoveredWidget = NULL; Widget *gFocusedWidget = NULL; -int gGuiFrame; Scene *gScene = NULL; diff --git a/src/widgets/FramebufferWidget.cpp b/src/widgets/FramebufferWidget.cpp index bb95ca00..ea2445dd 100644 --- a/src/widgets/FramebufferWidget.cpp +++ b/src/widgets/FramebufferWidget.cpp @@ -9,9 +9,16 @@ namespace rack { +/** A margin in pixels around the children in the framebuffer +This prevents cutting the rendered SVG off on the box edges. +*/ +static const float oversample = 2.0; + + struct FramebufferWidget::Internal { NVGLUframebuffer *fb = NULL; Rect box; + Vec lastS; ~Internal() { setFramebuffer(NULL); @@ -33,10 +40,31 @@ FramebufferWidget::~FramebufferWidget() { } void FramebufferWidget::draw(NVGcontext *vg) { + // Bypass framebuffer rendering entirely + // Widget::draw(vg); + // return; + + // Get world transform + float xform[6]; + nvgCurrentTransform(vg, xform); + // Skew is not supported + assert(fabsf(xform[1]) < 1e-6); + assert(fabsf(xform[2]) < 1e-6); + Vec s = Vec(xform[0], xform[3]); + Vec b = Vec(xform[4], xform[5]); + + if (gGuiFrame % 10 == 0) { + // Check if scale has changed + if (s.x != internal->lastS.x || s.y != internal->lastS.y) { + dirty = true; + } + internal->lastS = s; + } + + // Render to framebuffer if (dirty) { internal->box.pos = Vec(0, 0); - internal->box.size = box.size; - internal->box.size = Vec(ceilf(internal->box.size.x), ceilf(internal->box.size.y)); + internal->box.size = box.size.mult(s).ceil(); Vec fbSize = internal->box.size.mult(gPixelRatio * oversample); // assert(fbSize.isFinite()); @@ -60,6 +88,8 @@ void FramebufferWidget::draw(NVGcontext *vg) { nvgBeginFrame(gFramebufferVg, fbSize.x, fbSize.y, gPixelRatio * oversample); nvgScale(gFramebufferVg, gPixelRatio * oversample, gPixelRatio * oversample); + // Use local scaling + nvgScale(gFramebufferVg, s.x, s.y); Widget::draw(gFramebufferVg); nvgEndFrame(gFramebufferVg); @@ -72,32 +102,24 @@ void FramebufferWidget::draw(NVGcontext *vg) { return; } - // Draw framebuffer image + // Draw framebuffer image, using world coordinates + b = b.round(); + nvgSave(vg); + nvgResetTransform(vg); + nvgTranslate(vg, b.x, b.y); + 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); nvgFillPaint(vg, paint); nvgFill(vg); - // For debugging bounding box of framebuffer image - // nvgFillColor(vg, nvgRGBA(255, 0, 0, 64)); - // nvgFill(vg); - - { - float xform[6]; - nvgCurrentTransform(vg, xform); - // printf("%f %f %f %f; %f %f\n", xform[0], xform[1], xform[2], xform[3], xform[4], xform[5]); - nvgSave(vg); - nvgResetTransform(vg); - nvgTranslate(vg, xform[4], xform[5]); - nvgScale(vg, xform[0], xform[3]); - nvgBeginPath(vg); - nvgRect(vg, 0, 0, internal->box.size.x, internal->box.size.y); - nvgStrokeWidth(vg, 2.0); - nvgStrokeColor(vg, nvgRGBf(1.0, 0.0, 0.0)); - nvgStroke(vg); - nvgRestore(vg); - } + // For debugging the bounding box of the framebuffer + // nvgStrokeWidth(vg, 2.0); + // nvgStrokeColor(vg, nvgRGBA(255, 0, 0, 128)); + // nvgStroke(vg); + + nvgRestore(vg); } int FramebufferWidget::getImageHandle() { diff --git a/src/widgets/Scene.cpp b/src/widgets/Scene.cpp index a5de343a..9042871b 100644 --- a/src/widgets/Scene.cpp +++ b/src/widgets/Scene.cpp @@ -1,4 +1,5 @@ #include "widgets.hpp" +#include "gui.hpp" namespace rack { diff --git a/src/widgets/Slider.cpp b/src/widgets/Slider.cpp index 9727eefd..ae82fd24 100644 --- a/src/widgets/Slider.cpp +++ b/src/widgets/Slider.cpp @@ -25,5 +25,11 @@ void Slider::onDragEnd() { guiCursorUnlock(); } +void Slider::onMouseDownOpaque(int button) { + if (button == 1) { + setValue(defaultValue); + } +} + } // namespace rack diff --git a/src/widgets/TextField.cpp b/src/widgets/TextField.cpp index c98b2054..f3703f36 100644 --- a/src/widgets/TextField.cpp +++ b/src/widgets/TextField.cpp @@ -18,6 +18,10 @@ void TextField::draw(NVGcontext *vg) { state = BND_DEFAULT; bndTextField(vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_NONE, state, -1, text.c_str(), begin, end); + // Draw placeholder text + if (text.empty() && state != BND_ACTIVE) { + bndIconLabelCaret(vg, 0.0, 0.0, box.size.x, box.size.y, -1, bndGetTheme()->textFieldTheme.itemColor, 13, placeholder.c_str(), bndGetTheme()->textFieldTheme.itemColor, 0, -1); + } } Widget *TextField::onMouseDown(Vec pos, int button) { diff --git a/src/widgets/Tooltip.cpp b/src/widgets/Tooltip.cpp index 91f34b4e..bc177264 100644 --- a/src/widgets/Tooltip.cpp +++ b/src/widgets/Tooltip.cpp @@ -1,4 +1,5 @@ #include "widgets.hpp" +#include "gui.hpp" namespace rack { From bd10e6f1fa1902bb46f915229b1a0acc4ad0074e Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Tue, 24 Oct 2017 09:17:46 -0400 Subject: [PATCH 3/3] Made Framebuffer scaling work with real numbered positions --- src/widgets/FramebufferWidget.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/widgets/FramebufferWidget.cpp b/src/widgets/FramebufferWidget.cpp index ea2445dd..eeaf9a3c 100644 --- a/src/widgets/FramebufferWidget.cpp +++ b/src/widgets/FramebufferWidget.cpp @@ -53,18 +53,16 @@ void FramebufferWidget::draw(NVGcontext *vg) { Vec s = Vec(xform[0], xform[3]); Vec b = Vec(xform[4], xform[5]); - if (gGuiFrame % 10 == 0) { - // Check if scale has changed - if (s.x != internal->lastS.x || s.y != internal->lastS.y) { - dirty = true; - } - internal->lastS = s; + // Check if scale has changed + if (s.x != internal->lastS.x || s.y != internal->lastS.y) { + dirty = true; } + internal->lastS = s; // Render to framebuffer if (dirty) { internal->box.pos = Vec(0, 0); - internal->box.size = box.size.mult(s).ceil(); + internal->box.size = box.size.mult(s).ceil().plus(Vec(1, 1)); Vec fbSize = internal->box.size.mult(gPixelRatio * oversample); // assert(fbSize.isFinite()); @@ -89,6 +87,8 @@ void FramebufferWidget::draw(NVGcontext *vg) { nvgScale(gFramebufferVg, gPixelRatio * oversample, gPixelRatio * oversample); // Use local scaling + Vec bFrac = Vec(fmodf(b.x, 1.0), fmodf(b.y, 1.0)); + nvgTranslate(gFramebufferVg, bFrac.x, bFrac.y); nvgScale(gFramebufferVg, s.x, s.y); Widget::draw(gFramebufferVg); @@ -103,7 +103,7 @@ void FramebufferWidget::draw(NVGcontext *vg) { } // Draw framebuffer image, using world coordinates - b = b.round(); + b = b.floor(); nvgSave(vg); nvgResetTransform(vg); nvgTranslate(vg, b.x, b.y);