#include "renderBase.h" #include "glfw.h" #include "settings.h" #include "drawelement.h" #include "interact.h" #include "picture.h" #ifdef HAVE_LIBGLM #ifdef HAVE_GLFW // Forward declaration for GLFWwindow to avoid including glfw3.h here struct GLFWwindow; #endif using settings::getSetting; namespace camp { AsyRender* gl; } // namespace camp using namespace glm; // Forward declaration for exit handler (defined in exithandlers.h) void exitHandler(int); namespace camp { void AsyRender::copyRenderArgs(RenderFunctionArgs const& args) { // Basic picture and format state pic = args.pic; Prefix = args.prefix; Format = args.format; remesh = true; // Lighting nlights = args.nlightsin; Lights = args.lights; LightsDiffuse = args.diffuse; Oldpid = args.oldpid; // Camera parameters Angle = args.angle * radians; lastzoom = 0; Zoom0 = std::fpclassify(args.zoom) == FP_NORMAL ? args.zoom : 1.0; Shift = args.shift / args.zoom; Margin = args.margin; // Background color for (int i = 0; i < 4; i++) Background[i] = static_cast(args.background[i]); // View settings ViewExport = args.view; View = args.view && !settings::getSetting("offscreen"); title = std::string(PACKAGE_NAME) + ": " + args.prefix.c_str(); // Scene bounds Xmin = args.m.getx(); Xmax = args.M.getx(); Ymin = args.m.gety(); Ymax = args.M.gety(); Zmin = args.m.getz(); Zmax = args.M.getz(); haveScene = Xmin < Xmax && Ymin < Ymax && Zmin < Zmax; orthographic = Angle == 0.0; H = orthographic ? 0.0 : -tan(0.5 * Angle) * Zmax; Xfactor = Yfactor = 1.0; // Transform matrices for (int i = 0; i < 16; ++i) T[i] = args.t[i]; for (int i = 0; i < 16; ++i) Tup[i] = args.tup[i]; } double AsyRender::getRenderResolution(triple Min) const { double prerender = settings::getSetting("prerender"); if (prerender <= 0.0) return 0.0; prerender = 1.0 / prerender; double perspective = orthographic || Zmax == 0.0 ? 0.0 : 1.0 / Zmax; double s = perspective ? Min.getz() * perspective : 1.0; triple b(Xmin, Ymin, Zmin); triple B(Xmax, Ymax, Zmax); pair size3(s * (B.getx() - b.getx()), s * (B.gety() - b.gety())); pair size2(Width, Height); return prerender * size3.length() / size2.length(); } // Default implementations for virtual methods that can have generic behavior void AsyRender::setDimensions(int Width, int Height, double X, double Y) { // Guard against zero dimensions to prevent division by zero (SIGFPE). if(Width <= 0) Width = 1; if(Height <= 0) Height = 1; double aspect = ((double) Width) / Height; double zoom = Zoom * zoomFactor; double xshift = (X / (double) Width + Shift.getx() * Xfactor) * zoom; double yshift = (Y / (double) Height + Shift.gety() * Yfactor) * zoom; double zoominv = 1.0 / zoom; if (orthographic) { double xsize = Xmax - Xmin; double ysize = Ymax - Ymin; if (xsize < ysize * aspect) { double r = 0.5 * ysize * aspect * zoominv; double X0 = 2.0 * r * xshift; double Y0 = ysize * zoominv * yshift; xmin = -r - X0; xmax = r - X0; ymin = Ymin * zoominv - Y0; ymax = Ymax * zoominv - Y0; } else { double r = 0.5 * xsize * zoominv / aspect; double X0 = xsize * zoominv * xshift; double Y0 = 2.0 * r * yshift; xmin = Xmin * zoominv - X0; xmax = Xmax * zoominv - X0; ymin = -r - Y0; ymax = r - Y0; } } else { double r = H * zoominv; double rAspect = r * aspect; double X0 = 2.0 * rAspect * xshift; double Y0 = 2.0 * r * yshift; xmin = -rAspect - X0; xmax = rAspect - X0; ymin = -r - Y0; ymax = r - Y0; } } void AsyRender::setProjection() { setDimensions(Width, Height, X, Y); if(haveScene) { if(orthographic) ortho(xmin,xmax,ymin,ymax,-Zmax,-Zmin); else frustum(xmin,xmax,ymin,ymax,-Zmax,-Zmin); } } void AsyRender::updateModelViewData() { // Update normal matrix (inverse transpose of view matrix rotation) normMat = dmat3(inverse(viewMat)); } void AsyRender::update() { capzoom(); double cz = 0.5 * (Zmin + Zmax); viewMat = translate(translate(dmat4(1.0), dvec3(cx, cy, cz)) * rotateMat, dvec3(0, 0, -cz)); setProjection(); updateModelViewData(); redraw=true; #ifdef HAVE_PTHREAD #ifdef HAVE_LIBGLFW if(View) { pthread_t postThread; if(pthread_create(&postThread,NULL,postEmptyEvent,NULL) == 0) pthread_join(postThread,NULL); } #endif #endif } void AsyRender::updateProjection() { projViewMat = projMat * viewMat; } void AsyRender::frustum(double left, double right, double bottom, double top, double nearVal, double farVal) { projMat = glm::frustum(left, right, bottom, top, nearVal, farVal); updateProjection(); } void AsyRender::ortho(double left, double right, double bottom, double top, double nearVal, double farVal) { projMat = glm::ortho(left, right, bottom, top, nearVal, farVal); updateProjection(); } void AsyRender::clearCenters() { drawElement::centers.clear(); drawElement::centermap.clear(); } void AsyRender::clearMaterials() { materials.clear(); materialMap.clear(); } void AsyRender::prepareScene() { #ifdef HAVE_PTHREAD static bool first=true; if(threads && first) { threadMgr.wait(threadMgr.initSignal,threadMgr.initLock); threadMgr.endwait(threadMgr.initSignal,threadMgr.initLock); first=false; } if(format3dWait) threadMgr.wait(threadMgr.initSignal,threadMgr.initLock); #endif if(redraw) { clearData(); if(remesh) clearCenters(); triple m(xmin,ymin,Zmin); triple M(xmax,ymax,Zmax); double perspective=orthographic || Zmax == 0.0 ? 0.0 : 1.0/Zmax; double size2=hypot(Width,Height); pic->render(size2,m,M,perspective,remesh); redraw=false; if(mode != DRAWMODE_OUTLINE) remesh=false; setOpaque(); } } projection AsyRender::camera(bool user) { camp::Triple vCamera, vUp, vTarget; double cz = 0.5 * (Zmin + Zmax); double *Rotate = value_ptr(rotateMat); if(user) { double shift[]={0.0,0.0,0.0,0.0}; for(int i=0; i < 3; ++i) { double sumCamera=0.0, sumTarget=0.0, sumUp=0.0; int i4=4*i; shift[3]=T[i4+2]*cz; for(int j=0; j < 4; ++j) { int j4=4*j; double R0=Rotate[j4]; double R1=Rotate[j4+1]; double R2=Rotate[j4+2]; double R3=Rotate[j4+3]; double T4ij=T[i4+j]+shift[j]; // T -> T*shift(0,0,cz); sumCamera += T4ij*(R3-cx*R0-cy*R1-cz*R2); sumUp += Tup[i4+j]*R1; sumTarget += T4ij*(R3-cx*R0-cy*R1); } vCamera[i]=sumCamera; vUp[i]=sumUp; vTarget[i]=sumTarget; } } else { for(int i=0; i < 3; ++i) { int i4=4*i; double R0=Rotate[i4]; double R1=Rotate[i4+1]; double R2=Rotate[i4+2]; double R3=Rotate[i4+3]; vCamera[i]=R3-cx*R0-cy*R1-cz*R2; vUp[i]=R1; vTarget[i]=R3-cx*R0-cy*R1; } } return projection(orthographic, vCamera, vUp, vTarget, Zoom, 2.0*atan(tan(0.5*Angle)/Zoom)/radians, pair(X/Width+Shift.getx(), Y/Height+Shift.gety())); } void AsyRender::showCamera() { projection P = camera(); string projectionStr = P.orthographic ? "orthographic(" : "perspective("; string indent(2 + projectionStr.length(), ' '); cout << endl << "currentprojection=" << endl << " " << projectionStr << "camera=" << P.camera << "," << endl << indent << "up=" << P.up << "," << endl << indent << "target=" << P.target << "," << endl << indent << "zoom=" << P.zoom; if(!orthographic) cout << "," << endl << indent << "angle=" << P.angle; if(P.viewportshift != pair(0.0,0.0)) cout << "," << endl << indent << "viewportshift=" << P.viewportshift*Zoom; if(!orthographic) cout << "," << endl << indent << "autoadjust=false"; cout << ");" << endl; } void AsyRender::shift(double dx, double dy) { double Zoominv = 1.0 / Zoom; X += dx * Zoominv; Y += -dy * Zoominv; update(); } void AsyRender::pan(double dx, double dy) { if(orthographic) shift(dx, dy); else { cx += dx * (xmax - xmin) / Width; cy -= dy * (ymax - ymin) / Height; update(); } } void AsyRender::capzoom() { static double maxzoom = sqrt(DBL_MAX); static double minzoom = 1.0 / maxzoom; if(Zoom <= minzoom) Zoom = minzoom; if(Zoom >= maxzoom) Zoom = maxzoom; if(fabs(Zoom - lastzoom) > settings::getSetting("zoomThreshold")) { remesh = true; lastzoom = Zoom; } } void AsyRender::zoom(double dx, double dy) { double zoomFactor = settings::getSetting("zoomfactor"); if (zoomFactor > 0.0) { double zoomStep = settings::getSetting("zoomstep"); const double limit = log(0.1*DBL_MAX) / log(zoomFactor); double stepPower = zoomStep * dy; if(fabs(stepPower) < limit) { Zoom *= std::pow(zoomFactor, -stepPower); update(); } } } void AsyRender::capsize(int& width, int& height) { if(width > screenWidth) width = screenWidth; if(height > screenHeight) height = screenHeight; } void AsyRender::fitAspect(int& w, int& h) { if(w > h * Aspect) w = (int) std::ceil(h * Aspect); else h = (int) std::ceil(w / Aspect); } void AsyRender::windowposition(int& x, int& y, int width, int height) { if (width == -1) { width = Width; } if (height == -1) { height = Height; } pair z = settings::getSetting("position"); x = (int) z.getx(); y = (int) z.gety(); if(x < 0) { x += screenWidth - width; if(x < 0) x = 0; } if(y < 0) { y += screenHeight - height; if(y < 0) y = 0; } } /** * Set window to fullscreen size. * Base implementation handles dimension calculation and GLFW window operations. */ void AsyRender::fullscreen(bool reposition) { Xfactor = Yfactor = 1.0; if (screenWidth < screenHeight * Aspect) zoomFactor = (double)screenWidth / (screenHeight * Aspect); else zoomFactor = 1.0; setsize(screenWidth, screenHeight, reposition); reshape(screenWidth, screenHeight); } /** * Handle window resize. * Base implementation handles dimension updates and projection. */ void AsyRender::reshape(int width, int height) { // Scale X,Y proportionally with new dimensions X = (X / Width) * width; Y = (Y / Height) * height; Width = width; Height = height; static int lastWidth = 1; static int lastHeight = 1; if (View && width * height > 1 && (width != lastWidth || height != lastHeight)) { if (settings::verbose > 1) cout << "Rendering " << stripDir(Prefix) << " as " << width << "x" << height << " image" << endl; lastWidth = width; lastHeight = height; } setProjection(); } void AsyRender::fitscreen(bool reposition) { remesh = true; switch(Fitscreen) { case 0: // Original size: use saved framebuffer dimensions { Xfactor = Yfactor = 1.0; zoomFactor = 1.0; setsize(oldWidth, oldHeight, reposition); break; } case 1: // Fit to screen: screenWidth/screenHeight already physical pixels { zoomFactor = 1.0; int w = screenWidth; int h = screenHeight; fitAspect(w, h); setsize(w, h, reposition); reshape(w,h); break; } case 2: // Full screen: fill physical screen directly { fullscreen(reposition); break; } } } void AsyRender::toggleFitScreen() { Fitscreen = (Fitscreen + 1) % 3; fitscreen(); } void AsyRender::home() { idle(); X = Y = cx = cy = 0; rotateMat = viewMat = dmat4(1.0); lastzoom = Zoom = Zoom0; framecount = 0; remesh = true; setProjection(); updateModelViewData(); } void AsyRender::cycleMode() { mode = DrawMode((mode + 1) % NUM_DRAW_MODES); remesh = true; redraw = true; // Update IBL setting based on mode if (mode == DRAWMODE_NORMAL) { ibl = settings::getSetting("ibl"); } else if (mode == DRAWMODE_OUTLINE) { ibl = false; } } double AsyRender::spinStep() { return settings::getSetting("spinstep") * spinTimer.seconds(true); } void AsyRender::rotateX(double step) { dmat4 tmpRot(1.0); tmpRot = rotate(tmpRot, glm::radians(step), dvec3(1, 0, 0)); rotateMat = tmpRot * rotateMat; update(); } void AsyRender::rotateY(double step) { dmat4 tmpRot(1.0); tmpRot = rotate(tmpRot, glm::radians(step), dvec3(0, 1, 0)); rotateMat = tmpRot * rotateMat; update(); } void AsyRender::rotateZ(double step) { dmat4 tmpRot(1.0); tmpRot = rotate(tmpRot, glm::radians(step), dvec3(0, 0, 1)); rotateMat = tmpRot * rotateMat; update(); } void AsyRender::xspin() { rotateX(spinStep()); } void AsyRender::yspin() { rotateY(spinStep()); } void AsyRender::zspin() { rotateZ(spinStep()); } void AsyRender::spinx() { if(Xspin) idle(); else { idleFunc([this](){xspin();}); Xspin = true; Yspin = Zspin = false; } } void AsyRender::spiny() { if(Yspin) idle(); else { idleFunc([this](){yspin();}); Yspin = true; Xspin = Zspin = false; } } void AsyRender::spinz() { if(Zspin) idle(); else { idleFunc([this](){zspin();}); Zspin = true; Xspin = Yspin = false; } } void AsyRender::idleFunc(std::function f) { spinTimer.reset(); currentIdleFunc = f; } void AsyRender::idle() { idleFunc(nullptr); Xspin = Yspin = Zspin = false; } void AsyRender::expand() { double resizeStep = settings::getSetting("resizestep"); if(resizeStep > 0.0) setsize((int) (Width*resizeStep+0.5), (int) (Height*resizeStep+0.5)); } void AsyRender::shrink() { double resizeStep = settings::getSetting("resizestep"); if(resizeStep > 0.0) setsize(max((int) (Width/resizeStep+0.5),1), max((int) (Height/resizeStep+0.5),1)); } /** * Process messages from the message queue (inter-thread communication). */ void AsyRender::processMessages(RendererMessage const& msg) { switch (msg) { case RendererMessage::exportRender: if (readyForExport) { readyForExport=false; exportHandler(0); } break; case RendererMessage::updateRenderer: updateHandler(0); break; default: break; } } /** * Finalize graphics library resources. * Default implementation does nothing; derived classes override for specific cleanup. */ void AsyRender::finalizeProcess() { // Default: no-op } /** * Display/render the current frame (library-agnostic implementation). * Uses virtual hooks for library-specific operations. */ void AsyRender::display() { prepareScene(); // Show window if needed (library-specific) showWindow(); // Draw the frame (renderer-specific) drawFrame(); // FPS tracking bool fps = settings::verbose > 2; if(fps) { if(framecount < 20) fpsTimer.reset(); else { double s = fpsTimer.seconds(true); if(s > 0.0) { double rate = 1.0/s; fpsStats.add(rate); if(framecount % 20 == 0) cout << "FPS=" << rate << "\t" << fpsStats.mean() << " +/- " << fpsStats.stdev() << endl; } } ++framecount; } // Swap buffers (library-specific) swapBuffers(); // Process management (non-Windows) if(!threads) { #if defined(_WIN32) #else if(Oldpid != 0 && waitpid(Oldpid, NULL, WNOHANG) != Oldpid) { kill(Oldpid, SIGHUP); Oldpid = 0; } #endif } } /** * Swap front and back buffers (library-specific). * Default: no-op - override in derived classes. */ void AsyRender::swapBuffers() { // Default: no-op - derived classes must override } #ifdef HAVE_LIBGLFW /** * Show the window if hidden (GLFW-specific implementation). */ void AsyRender::showWindow() { GLFWwindow* win = glfwWindow; if(View && !hideWindow && !glfwGetWindowAttrib(win, GLFW_VISIBLE)) ::glfwShowWindow(win); } #else // Stub for when GLFW is unavailable (satisfies vtable) void AsyRender::showWindow() {} #endif /** * Window close handler (library-agnostic). */ void AsyRender::onClose() { // Default: no-op - derived classes can override for specific behavior } #ifdef HAVE_LIBGLFW void AsyRender::onKey(int key, int scancode, int action, int mods) { if (action != GLFW_PRESS) return; switch (key) { case 'H': home(); redraw = true; break; case 'F': toggleFitScreen(); break; case 'X': spinx(); break; case 'Y': spiny(); break; case 'Z': spinz(); break; case 'S': idle(); break; case 'M': cycleMode(); break; case 'E': queueExport = true; redraw = true; break; case 'C': showCamera(); break; case '.': // '>' = '.' + shift if (!(mods & GLFW_MOD_SHIFT)) break; case '+': case '=': expand(); break; case ',': // '<' = ',' + shift if (!(mods & GLFW_MOD_SHIFT)) break; case '-': case '_': shrink(); break; case 'Q': if(!Format.empty()) exportHandler(0); quit(); break; } } void AsyRender::onScroll(double xoffset, double yoffset) { std::string action = getGLFWScrollAction(yoffset <= 0); auto zoomFactor = getSetting("zoomfactor"); if(action == "zoomin" || action.empty()) { if(zoomFactor > 0.0) Zoom /= zoomFactor; } else if(action == "zoomout") { if(zoomFactor > 0.0) Zoom *= zoomFactor; } update(); } #else // !HAVE_LIBGLFW // Stubs for when GLFW is unavailable (satisfy vtable) void AsyRender::onKey(int, int, int, int) {} void AsyRender::onScroll(double, double) {} void AsyRender::onMouseButton(int, int, int) {} #endif // HAVE_LIBGLFW void AsyRender::onCursorPos(double xpos, double ypos) { if (lastAction == "rotate") { Arcball arcball(xprev * 2 / Width - 1, 1 - yprev * 2 / Height, xpos * 2 / Width - 1, 1 - ypos * 2 / Height); triple axis = arcball.axis; rotateMat = rotate(2 * arcball.angle / Zoom * ArcballFactor, dvec3(axis.getx(), axis.gety(), axis.getz())) * rotateMat; update(); } else if (lastAction == "shift") { shift(xpos - xprev, ypos - yprev); update(); } else if (lastAction == "pan") { if (orthographic) shift(xpos - xprev, ypos - yprev); else pan(xpos - xprev, ypos - yprev); update(); } else if (lastAction == "zoom") { zoom(0.0, ypos - yprev); } xprev = xpos; yprev = ypos; } void AsyRender::onFramebufferResize(int width, int height) { if(width == 0 || height == 0) return; if(width == Width && height == Height) return; reshape(width, height); update(); remesh = true; } #ifdef HAVE_LIBGLFW void AsyRender::mainLoop() { if(View) { GLFWwindow* win = glfwWindow; glfwRunLoop(win, // shouldContinue: continue while window is open [win](){ return !glfwWindowShouldClose(win); }, // shouldDisplay: display when needed [this](){ return redraw || redisplay || queueExport; }, // doDisplay: handle display logic [this](){ redisplay=false; waitEvent=true; if(resize) { fitscreen(!interact::interactive); resize=false; } display(); }, // processMessages: dequeue and process messages #ifdef HAVE_PTHREAD [this](){ auto const message=threadMgr.messageQueue.dequeue(); if(message.has_value()) processMessages(*message); }, #else [](){}, #endif // getIdleFunc: return current idle function (or nullptr) [this](){ return currentIdleFunc; }, // shouldWait: use waitEvent to decide between wait and poll [this](){ return waitEvent; } ); } else { update(); display(); if(threads) { if(havewindow) { #ifdef HAVE_PTHREAD if(pthread_equal(pthread_self(),threadMgr.mainthread)) exportHandler(0); else threadMgr.messageQueue.enqueue(RendererMessage::exportRender); #endif } else { initialized=true; readyForExport=true; exportHandler(0); #ifdef HAVE_PTHREAD // Notify main thread only when called from the render thread. // When called directly from the main thread, there's nobody to wake. if(!pthread_equal(pthread_self(), threadMgr.mainthread)) threadMgr.endwait(threadMgr.readySignal, threadMgr.readyLock); #endif } } else { exportHandler(0); quit(); } } } #else // !HAVE_LIBGLFW // Stub for when GLFW is unavailable (satisfies vtable) void AsyRender::mainLoop() {} #endif // HAVE_LIBGLFW #ifdef HAVE_RENDERER // ========================================================================= // Consolidated renderer-specific function definitions. // All functions below require HAVE_RENDERER (implies HAVE_LIBGLM + HAVE_LIBGLFW). // They are called only from vkrender.cc and glrender.cc. // ========================================================================= // Matrix accessor functions - shared between GL and Vulkan renderers. // These delegate to the corresponding AsyRender member functions. const glm::dmat4& getProjViewMat() { return gl->getProjViewMat(); } const glm::dmat4& getViewMat() { return gl->getViewMat(); } const glm::dmat3& getNormMat() { return gl->getNormMat(); } void AsyRender::initDisplay(int contentWidth, int contentHeight) { // Compute expand/fullWidth/fullHeight (unscaled content dimensions). double expand = settings::getSetting("render"); if (expand < 0) expand *= (Format.empty() || Format == "eps" || Format == "pdf") ? -2.0 : -1.0; if (antialias) expand *= 2.0; fullWidth = (int) std::ceil(expand * contentWidth); fullHeight = (int) std::ceil(expand * contentHeight); oWidth = contentWidth; oHeight = contentHeight; GLFWmonitor* monitor = NULL; glfwInit(); devicePixelRatio = settings::getSetting("devicepixelratio"); monitor = glfwGetPrimaryMonitor(); if (monitor) { int mx, my; glfwGetMonitorWorkarea(monitor, &mx, &my, &screenWidth, &screenHeight); if (devicePixelRatio <= 0.0) { float sx = 1.0f, sy = 1.0f; glfwGetMonitorContentScale(monitor, &sx, &sy); devicePixelRatio = std::max(sx, sy); } } else { screenWidth = fullWidth; screenHeight = fullHeight; } oldWidth = (int) std::ceil(contentWidth * devicePixelRatio); oldHeight = (int) std::ceil(contentHeight * devicePixelRatio); int w = std::min(oldWidth, screenWidth); int h = std::min(oldHeight, screenHeight); fitAspect(w, h); if(View) { Width = w; Height = h; } else { // For offscreen rendering, use the expanded dimensions. // OpenGL uses fullWidth/fullHeight in its Export() tiling loop; Vulkan needs // Width/Height to reflect the expanded size so createOffscreenBuffers() allocates // frames at the correct resolution. Width = fullWidth; Height = fullHeight; } // Guard against zero dimensions (e.g., headless rendering with no monitor) // to avoid division by zero in setDimensions() and ArcballFactor computation. if(Width <= 0) Width = 1; if(Height <= 0) Height = 1; home(); ArcballFactor = 1 + 8.0 * hypot(Margin.getx(), Margin.gety()) / hypot(Width, Height); } void AsyRender::clearData() { pointData.clear(); lineData.clear(); materialData.clear(); colorData.clear(); triangleData.clear(); transparentData.clear(); } void AsyRender::setOpaque() { Opaque = transparentData.indices.empty(); } void AsyRender::exportHandler(int) { readyAfterExport = true; } void AsyRender::setsize(int w, int h, bool reposition) { capsize(w, h); if (View && glfwWindow != nullptr) { GLFWwindow* win = glfwWindow; ::glfwSetWindowSize(win, w, h); if (reposition) { int x, y; windowposition(x, y, w, h); ::glfwSetWindowPos(win, x, y); } } update(); } void AsyRender::updateHandler(int) { if (View && !interact::interactive) { ::glfwHideWindow(glfwWindow); if (!getSetting("fitscreen")) Fitscreen = 0; } resize = true; redisplay = true; redraw = true; remesh = true; waitEvent = false; } void AsyRender::quit() { resize = false; waitEvent = false; redraw = false; if (threads) { #ifdef HAVE_PTHREAD if (!interact::interactive) { idle(); threadMgr.endwait(threadMgr.readySignal, threadMgr.readyLock); } #endif if (View && glfwWindow) { ::glfwHideWindow(glfwWindow); hideWindow = true; } } else { finalizeProcess(); if (View && glfwWindow) { ::glfwDestroyWindow(glfwWindow); glfwWindow = nullptr; } glfwTerminate(); exit(0); } } void AsyRender::onMouseButton(int button, int action, int mods) { auto const currentActionStr = getGLFWAction(button, mods); if (currentActionStr.empty()) return; if (action == GLFW_PRESS) { lastAction = currentActionStr; double xpos, ypos; glfwGetCursorPos(glfwWindow, &xpos, &ypos); xprev = xpos; yprev = ypos; } else if (action == GLFW_RELEASE) { lastAction.clear(); } } #else // !HAVE_RENDERER // Stubs for when GLFW/Vulkan/GL are unavailable (satisfy vtable and link). void AsyRender::clearData() {} void AsyRender::setOpaque() {} void AsyRender::exportHandler(int) { readyAfterExport = true; } void AsyRender::setsize(int, int, bool) {} void AsyRender::updateHandler(int) {} void AsyRender::quit() { exit(0); } #endif // HAVE_RENDERER } // namespace camp #endif // HAVE_LIBGLM