| @@ -141,6 +141,7 @@ struct RackWidget : OpaqueWidget { | |||
| Widget *onMouseMove(Vec pos, Vec mouseRel) override; | |||
| void onMouseDownOpaque(int button) override; | |||
| void onZoom() override; | |||
| }; | |||
| struct RackRail : TransparentWidget { | |||
| @@ -107,18 +107,6 @@ struct Davies1900hLargeRedKnob : Davies1900hKnob { | |||
| } | |||
| }; | |||
| struct Davies1900hSmallBlackKnob : Davies1900hKnob { | |||
| Davies1900hSmallBlackKnob() { | |||
| setSVG(SVG::load(assetGlobal("res/ComponentLibrary/Davies1900hSmallBlack.svg"))); | |||
| } | |||
| }; | |||
| struct Davies1900hSmallBlackSnapKnob : Davies1900hSmallBlackKnob { | |||
| Davies1900hSmallBlackSnapKnob() { | |||
| snap = true; | |||
| } | |||
| }; | |||
| struct Rogan : SVGKnob { | |||
| Rogan() { | |||
| @@ -197,9 +197,15 @@ struct Vec { | |||
| Vec ceil() { | |||
| return Vec(ceilf(x), ceilf(y)); | |||
| } | |||
| bool isEqual(Vec b) { | |||
| return x == b.x && y == b.y; | |||
| } | |||
| bool isZero() { | |||
| return x == 0.0 && y == 0.0; | |||
| } | |||
| bool isFinite() { | |||
| return isfinite(x) && isfinite(y); | |||
| } | |||
| Vec clamp(Rect bound); | |||
| }; | |||
| @@ -229,6 +235,9 @@ struct Rect { | |||
| 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); | |||
| } | |||
| bool isEqual(Rect r) { | |||
| return pos.isEqual(r.pos) && size.isEqual(r.size); | |||
| } | |||
| Vec getCenter() { | |||
| return pos.plus(size.mult(0.5)); | |||
| } | |||
| @@ -241,8 +250,17 @@ struct Rect { | |||
| Vec getBottomRight() { | |||
| return pos.plus(size); | |||
| } | |||
| /** Clamps the position to fix inside a bounding box */ | |||
| /** Clamps the edges of the rectangle to fit within a bound */ | |||
| Rect clamp(Rect bound) { | |||
| Rect r; | |||
| r.pos.x = clampf(pos.x, bound.pos.x, bound.pos.x + bound.size.x); | |||
| r.pos.y = clampf(pos.y, bound.pos.y, bound.pos.y + bound.size.y); | |||
| r.size.x = clampf(pos.x + size.x, bound.pos.x, bound.pos.x + bound.size.x) - r.pos.x; | |||
| r.size.y = clampf(pos.y + size.y, bound.pos.y, bound.pos.y + bound.size.y) - r.pos.y; | |||
| return r; | |||
| } | |||
| /** Nudges the position to fix inside a bounding box */ | |||
| Rect nudge(Rect bound) { | |||
| Rect r; | |||
| r.size = size; | |||
| r.pos.x = clampf(pos.x, bound.pos.x, bound.pos.x + bound.size.x - size.x); | |||
| @@ -58,6 +58,8 @@ struct Widget { | |||
| Vec getAbsolutePos(); | |||
| Rect getChildrenBoundingBox(); | |||
| /** Returns a subset of the given Rect bounded by the box of this widget and all ancestors */ | |||
| virtual Rect getViewport(Rect r); | |||
| template <class T> | |||
| T *getAncestorOfType() { | |||
| @@ -131,6 +133,7 @@ struct Widget { | |||
| virtual void onAction() {} | |||
| virtual void onChange() {} | |||
| virtual void onZoom(); | |||
| }; | |||
| struct TransformWidget : Widget { | |||
| @@ -144,6 +147,17 @@ struct TransformWidget : Widget { | |||
| void draw(NVGcontext *vg) override; | |||
| }; | |||
| struct ZoomWidget : Widget { | |||
| float zoom = 1.0; | |||
| Rect getViewport(Rect r) override; | |||
| void setZoom(float zoom); | |||
| void draw(NVGcontext *vg) override; | |||
| Widget *onMouseDown(Vec pos, int button) override; | |||
| Widget *onMouseUp(Vec pos, int button) override; | |||
| Widget *onMouseMove(Vec pos, Vec mouseRel) override; | |||
| Widget *onHoverKey(Vec pos, int key) override; | |||
| Widget *onScroll(Vec pos, Vec scrollRel) override; | |||
| }; | |||
| //////////////////// | |||
| // Trait widgets | |||
| @@ -214,7 +228,7 @@ When `dirty` is true, its children will be re-rendered on the next call to step( | |||
| 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 */ | |||
| /** Set this to true to re-render the children to the framebuffer the next time it is drawn */ | |||
| bool dirty = true; | |||
| /** The root object in the framebuffer scene | |||
| The FramebufferWidget owns the pointer | |||
| @@ -226,6 +240,7 @@ struct FramebufferWidget : virtual Widget { | |||
| ~FramebufferWidget(); | |||
| void draw(NVGcontext *vg) override; | |||
| int getImageHandle(); | |||
| void onZoom() override; | |||
| }; | |||
| struct QuantityWidget : virtual Widget { | |||
| @@ -378,16 +393,6 @@ struct ScrollWidget : OpaqueWidget { | |||
| bool onScrollOpaque(Vec scrollRel) override; | |||
| }; | |||
| struct ZoomWidget : Widget { | |||
| float zoom = 1.0; | |||
| void draw(NVGcontext *vg) override; | |||
| Widget *onMouseDown(Vec pos, int button) override; | |||
| Widget *onMouseUp(Vec pos, int button) override; | |||
| Widget *onMouseMove(Vec pos, Vec mouseRel) override; | |||
| Widget *onHoverKey(Vec pos, int key) override; | |||
| Widget *onScroll(Vec pos, Vec scrollRel) override; | |||
| }; | |||
| struct TextField : OpaqueWidget { | |||
| std::string text; | |||
| std::string placeholder; | |||
| @@ -1,108 +0,0 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | |||
| <svg | |||
| xmlns:dc="http://purl.org/dc/elements/1.1/" | |||
| xmlns:cc="http://creativecommons.org/ns#" | |||
| xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |||
| xmlns:svg="http://www.w3.org/2000/svg" | |||
| xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
| xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
| width="7.0462589mm" | |||
| height="7.1788893mm" | |||
| viewBox="0 0 7.0462589 7.1788893" | |||
| version="1.1" | |||
| id="svg6414" | |||
| inkscape:version="0.92.1 r" | |||
| sodipodi:docname="Davies1900hSmallBlack.svg"> | |||
| <defs | |||
| id="defs6408"> | |||
| <clipPath | |||
| id="clip82"> | |||
| <path | |||
| d="m 979.55859,272.09375 h 26.63281 v 27.13281 h -26.63281 z m 0,0" | |||
| id="path22604" | |||
| inkscape:connector-curvature="0" /> | |||
| </clipPath> | |||
| <clipPath | |||
| id="clip83"> | |||
| <path | |||
| d="m 992,272.09375 h 2 V 287 h -2 z m 0,0" | |||
| id="path22607" | |||
| inkscape:connector-curvature="0" /> | |||
| </clipPath> | |||
| </defs> | |||
| <sodipodi:namedview | |||
| id="base" | |||
| pagecolor="#ffffff" | |||
| bordercolor="#666666" | |||
| borderopacity="1.0" | |||
| inkscape:pageopacity="0.0" | |||
| inkscape:pageshadow="2" | |||
| inkscape:zoom="0.98994949" | |||
| inkscape:cx="101.21858" | |||
| inkscape:cy="-36.897917" | |||
| inkscape:document-units="mm" | |||
| inkscape:current-layer="layer1" | |||
| showgrid="false" | |||
| fit-margin-top="0" | |||
| fit-margin-left="0" | |||
| fit-margin-right="0" | |||
| fit-margin-bottom="0" | |||
| inkscape:window-width="1600" | |||
| inkscape:window-height="882" | |||
| inkscape:window-x="0" | |||
| inkscape:window-y="18" | |||
| inkscape:window-maximized="0" /> | |||
| <metadata | |||
| id="metadata6411"> | |||
| <rdf:RDF> | |||
| <cc:Work | |||
| rdf:about=""> | |||
| <dc:format>image/svg+xml</dc:format> | |||
| <dc:type | |||
| rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |||
| <dc:title></dc:title> | |||
| </cc:Work> | |||
| </rdf:RDF> | |||
| </metadata> | |||
| <g | |||
| inkscape:label="Layer 1" | |||
| inkscape:groupmode="layer" | |||
| id="layer1" | |||
| transform="translate(-36.542344,-107.44627)"> | |||
| <g | |||
| id="g6344" | |||
| transform="matrix(0.35277777,0,0,-0.35277777,-474.41126,963.93894)"> | |||
| <g | |||
| style="clip-rule:nonzero" | |||
| id="g28551" | |||
| clip-path="url(#clip82)" | |||
| transform="matrix(0.75000002,0,0,-0.75000002,713.70253,2631.9236)"> | |||
| <path | |||
| inkscape:connector-curvature="0" | |||
| id="path28547" | |||
| d="m 1005.8555,288.88281 c -1.6407,7.16406 -8.78128,11.64844 -15.94925,10.00781 -7.16797,-1.64062 -11.64844,-8.78125 -10.00781,-15.94921 1.64062,-7.16797 8.78125,-11.64844 15.94922,-10.00782 7.16794,1.64063 11.64454,8.78125 10.00784,15.94922" | |||
| style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /> | |||
| <path | |||
| inkscape:connector-curvature="0" | |||
| id="path28549" | |||
| d="m 1005.8555,288.88281 c -1.6407,7.16406 -8.78128,11.64844 -15.94925,10.00781 -7.16797,-1.64062 -11.64844,-8.78125 -10.00781,-15.94921 1.64062,-7.16797 8.78125,-11.64844 15.94922,-10.00782 7.16794,1.64063 11.64454,8.78125 10.00784,15.94922" | |||
| style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /> | |||
| </g> | |||
| <g | |||
| style="clip-rule:nonzero" | |||
| id="g28555" | |||
| clip-path="url(#clip83)" | |||
| transform="matrix(0.75000002,0,0,-0.75000002,713.70253,2631.9236)"> | |||
| <path | |||
| inkscape:connector-curvature="0" | |||
| id="path28553" | |||
| transform="translate(992.87612,272.59527)" | |||
| d="M -0.0011146,-0.0015175 V 13.314889" | |||
| style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-opacity:1" /> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -63,7 +63,7 @@ void RackScene::step() { | |||
| .div(zoomWidget->zoom); | |||
| // Set zoom from the toolbar's zoom slider | |||
| zoomWidget->zoom = gToolbar->zoomSlider->value / 100.0; | |||
| zoomWidget->setZoom(gToolbar->zoomSlider->value / 100.0); | |||
| Scene::step(); | |||
| @@ -16,10 +16,13 @@ namespace rack { | |||
| RackWidget::RackWidget() { | |||
| rails = new FramebufferWidget(); | |||
| RackRail *rail = new RackRail(); | |||
| rail->box.size = Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT); | |||
| rails->addChild(rail); | |||
| rails->box.size = rail->box.size; | |||
| rails->box.size = Vec(); | |||
| { | |||
| RackRail *rail = new RackRail(); | |||
| rail->box.size = Vec(); | |||
| rails->addChild(rail); | |||
| } | |||
| addChild(rails); | |||
| moduleContainer = new Widget(); | |||
| addChild(moduleContainer); | |||
| @@ -29,7 +32,6 @@ RackWidget::RackWidget() { | |||
| } | |||
| RackWidget::~RackWidget() { | |||
| delete rails; | |||
| } | |||
| void RackWidget::clear() { | |||
| @@ -333,13 +335,29 @@ bool RackWidget::requestModuleBoxNearest(ModuleWidget *m, Rect box) { | |||
| } | |||
| void RackWidget::step() { | |||
| rails->step(); | |||
| // Expand size to fit modules | |||
| Vec moduleSize = moduleContainer->getChildrenBoundingBox().getBottomRight(); | |||
| // We assume that the size is reset by a parent before calling step(). Otherwise it will grow unbounded. | |||
| box.size = box.size.max(moduleSize); | |||
| // Adjust size and position of rails | |||
| Widget *rail = rails->children.front(); | |||
| Rect bound = getViewport(Rect(Vec(), box.size)); | |||
| if (!rails->box.contains(bound)) { | |||
| // Add a margin around the otherwise tight bound, so that scrolling slightly will not require a re-render of rails. | |||
| Vec margin = Vec(100, 100); | |||
| bound.pos = bound.pos.minus(margin); | |||
| bound.size = bound.size.plus(margin.mult(2)); | |||
| rails->box = bound; | |||
| // Compute offset of rail within rails framebuffer | |||
| Vec grid = Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT); | |||
| Vec gridPos = bound.pos.div(grid).floor().mult(grid); | |||
| bound.pos = gridPos.minus(bound.pos); | |||
| bound.size = bound.size.minus(bound.pos); | |||
| rail->box = bound; | |||
| rails->dirty = true; | |||
| } | |||
| // Autosave every 15 seconds | |||
| if (gGuiFrame % (60*15) == 0) { | |||
| savePatch(assetLocal("autosave.vcv")); | |||
| @@ -350,13 +368,6 @@ void RackWidget::step() { | |||
| } | |||
| void RackWidget::draw(NVGcontext *vg) { | |||
| // Draw rails | |||
| nvgBeginPath(vg); | |||
| nvgRect(vg, 0.0, 0.0, box.size.x, box.size.y); | |||
| NVGpaint paint = nvgImagePattern(vg, rails->box.pos.x, rails->box.pos.y, rails->box.size.x, rails->box.size.y, 0.0, rails->getImageHandle(), 1.0); | |||
| nvgFillPaint(vg, paint); | |||
| nvgFill(vg); | |||
| Widget::draw(vg); | |||
| } | |||
| @@ -513,5 +524,10 @@ void RackWidget::onMouseDownOpaque(int button) { | |||
| } | |||
| } | |||
| void RackWidget::onZoom() { | |||
| rails->box.size = Vec(); | |||
| Widget::onZoom(); | |||
| } | |||
| } // namespace rack | |||
| @@ -153,7 +153,7 @@ Toolbar::Toolbar() { | |||
| zoomSlider->box.size.x = 150; | |||
| zoomSlider->label = "Zoom"; | |||
| zoomSlider->unit = "%"; | |||
| zoomSlider->setLimits(25.0, 200.0); | |||
| zoomSlider->setLimits(33.33, 200.0); | |||
| zoomSlider->setDefaultValue(100.0); | |||
| addChild(zoomSlider); | |||
| xPos += zoomSlider->box.size.x; | |||
| @@ -18,7 +18,6 @@ static const float oversample = 2.0; | |||
| struct FramebufferWidget::Internal { | |||
| NVGLUframebuffer *fb = NULL; | |||
| Rect box; | |||
| Vec lastS; | |||
| ~Internal() { | |||
| setFramebuffer(NULL); | |||
| @@ -47,20 +46,16 @@ void FramebufferWidget::draw(NVGcontext *vg) { | |||
| // Get world transform | |||
| float xform[6]; | |||
| nvgCurrentTransform(vg, xform); | |||
| // Skew is not supported | |||
| // Skew and rotate 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]); | |||
| // 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) { | |||
| dirty = false; | |||
| internal->box.pos = Vec(0, 0); | |||
| internal->box.size = box.size.mult(s).ceil().plus(Vec(1, 1)); | |||
| Vec fbSize = internal->box.size.mult(gPixelRatio * oversample); | |||
| @@ -73,7 +68,7 @@ void FramebufferWidget::draw(NVGcontext *vg) { | |||
| // 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); | |||
| NVGLUframebuffer *fb = nvgluCreateFramebuffer(gVg, fbSize.x, fbSize.y, 0); | |||
| if (!fb) | |||
| return; | |||
| internal->setFramebuffer(fb); | |||
| @@ -94,8 +89,6 @@ void FramebufferWidget::draw(NVGcontext *vg) { | |||
| nvgEndFrame(gFramebufferVg); | |||
| nvgluBindFramebuffer(NULL); | |||
| dirty = false; | |||
| } | |||
| if (!internal->fb) { | |||
| @@ -128,5 +121,9 @@ int FramebufferWidget::getImageHandle() { | |||
| return internal->fb->image; | |||
| } | |||
| void FramebufferWidget::onZoom() { | |||
| dirty = true; | |||
| } | |||
| } // namespace rack | |||
| @@ -30,7 +30,7 @@ void Menu::setChildMenu(Menu *menu) { | |||
| void Menu::step() { | |||
| // Try to fit into the parent's box | |||
| if (parent) | |||
| box = box.clamp(Rect(Vec(0, 0), parent->box.size)); | |||
| box = box.nudge(Rect(Vec(0, 0), parent->box.size)); | |||
| Widget::step(); | |||
| @@ -40,6 +40,18 @@ Rect Widget::getChildrenBoundingBox() { | |||
| return Rect(topLeft, bottomRight.minus(topLeft)); | |||
| } | |||
| Rect Widget::getViewport(Rect r) { | |||
| Rect bound; | |||
| if (parent) { | |||
| bound = parent->getViewport(box); | |||
| } | |||
| else { | |||
| bound = box; | |||
| } | |||
| bound.pos = bound.pos.minus(box.pos); | |||
| return r.clamp(bound); | |||
| } | |||
| void Widget::addChild(Widget *widget) { | |||
| assert(!widget->parent); | |||
| widget->parent = this; | |||
| @@ -172,4 +184,11 @@ Widget *Widget::onScroll(Vec pos, Vec scrollRel) { | |||
| return NULL; | |||
| } | |||
| void Widget::onZoom() { | |||
| for (auto it = children.rbegin(); it != children.rend(); it++) { | |||
| Widget *child = *it; | |||
| child->onZoom(); | |||
| } | |||
| } | |||
| } // namespace rack | |||
| @@ -3,6 +3,20 @@ | |||
| namespace rack { | |||
| Rect ZoomWidget::getViewport(Rect r) { | |||
| r.pos = r.pos.mult(zoom); | |||
| r.size = r.size.mult(zoom); | |||
| r = Widget::getViewport(r); | |||
| r.pos = r.pos.div(zoom); | |||
| r.size = r.size.div(zoom); | |||
| return r; | |||
| } | |||
| void ZoomWidget::setZoom(float zoom) { | |||
| if (zoom != this->zoom) | |||
| onZoom(); | |||
| this->zoom = zoom; | |||
| } | |||
| void ZoomWidget::draw(NVGcontext *vg) { | |||
| nvgScale(vg, zoom, zoom); | |||