See More

#if __has_include("stb_truetype.h") # define PROCESSING_HAS_STB_TRUETYPE 1 # define STB_TRUETYPE_IMPLEMENTATION # include "stb_truetype.h" #else # define PROCESSING_HAS_STB_TRUETYPE 0 #endif #include "Processing.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // stb_image.h must be in the src/ folder for loadImage() to work. // Download from: https://github.com/nothings/stb/blob/master/stb_image.h #ifdef PROCESSING_HAS_STB_IMAGE #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #endif // Uncomment + drop stb_image_write.h to enable saveFrame()/save(): // #define STB_IMAGE_WRITE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" // ── Manual glu replacements (no GLU header needed) ─────────────────────────── static void _gluPerspective(double fovY_deg, double aspect, double zNear, double zFar) { // Identical to gluPerspective -- sets up a perspective projection matrix double f = 1.0 / std::tan(fovY_deg * M_PI / 360.0); double m[16] = {0}; m[0] = f / aspect; m[5] = f; m[10] = (zFar + zNear) / (zNear - zFar); m[11] = -1.0; m[14] = (2.0 * zFar * zNear) / (zNear - zFar); glMultMatrixd(m); } static void _gluLookAt(double ex,double ey,double ez, double cx,double cy,double cz, double ux,double uy,double uz) { // Identical to gluLookAt double fx=cx-ex, fy=cy-ey, fz=cz-ez; double len=std::sqrt(fx*fx+fy*fy+fz*fz); fx/=len;fy/=len;fz/=len; double rx=fy*uz-fz*uy, ry=fz*ux-fx*uz, rz=fx*uy-fy*ux; len=std::sqrt(rx*rx+ry*ry+rz*rz); rx/=len;ry/=len;rz/=len; double upx=ry*fz-rz*fy, upy=rz*fx-rx*fz, upz=rx*fy-ry*fx; double m[16]={ rx, upx, -fx, 0, ry, upy, -fy, 0, rz, upz, -fz, 0, 0, 0, 0, 1 }; glMultMatrixd(m); glTranslated(-ex,-ey,-ez); } // stb_truetype -- drop stb_truetype.h next to this file for TTF font rendering. // default.ttf in the project root is loaded automatically as the default font. // ============================================================================= // STATE // ============================================================================= // Window / canvas size static int fbW = 640, fbH = 480; // actual framebuffer (may be 2× on HiDPI) // Mouse state // Keyboard state // Frame timing // Current draw style // Lighting state // Color mode // User event callbacks std::function _onKeyPressed; std::function _onKeyReleased; std::function _onKeyTyped; std::function _onMousePressed; std::function _onMouseReleased; std::function _onMouseClicked; std::function _onMouseMoved; std::function _onMouseDragged; std::function _onMouseWheel; std::function _onWindowMoved; // On Windows: IDE.cpp stores a raw function pointer here before static init // of Processing.cpp completes. Raw function pointers are POD -- zero-initialized // at program start before ANY constructor runs, so this is always safe to write. // Shape state // ============================================================================= // NOISE - exact Java Processing implementation (PApplet.java) // Uses a 4096-entry float lookup table with cosine interpolation. // This matches Processing Java's noise() output exactly. // ============================================================================= namespace Processing { static void _doEnableDebugConsole() { #ifdef _WIN32 if (AllocConsole()) { FILE* f; freopen_s(&f, "CONOUT$", "w", stdout); freopen_s(&f, "CONOUT$", "w", stderr); freopen_s(&f, "CONIN$", "r", stdin); fprintf(stderr, "[debug] processing-cpp debug console enabled\n"); } #endif } void enableDebugConsole() { _doEnableDebugConsole(); } static std::string _s_objDir; // OBJ loader scratch static const int PERLIN_YWRAPB = 4; static const int PERLIN_YWRAP = 1 << PERLIN_YWRAPB; // 16 static const int PERLIN_ZWRAPB = 8; static const int PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB; // 256 static const int PERLIN_SIZE = 4095; void PApplet::initPerlin(unsigned int seed) { // Java Processing seeds with a simple LCG and fills with rand values in [0,1) // It uses its own random to not disturb the sketch's random() uint32_t s = seed; auto jrand = [&]() -> float { s = s * 1664525u + 1013904223u; // LCG like Java's Random return (s >> 8) / (float)(1 << 24); }; for (int i = 0; i < PERLIN_SIZE + 1; i++) perlinTable[i] = jrand(); perlinInit = true; } void PApplet::noiseSeed(int s) { initPerlin((unsigned int)s); } void PApplet::noiseDetail(int o, float f) { noiseOctaves = o; noiseFalloff = f; } // Cosine interpolation curve -- Java Processing's noise_fsc() static inline float noise_fsc(float i) { return 0.5f * (1.0f - std::cos(i * PI)); } float PApplet::noise(float x, float y, float z) { if (!perlinInit) initPerlin(0); // default seed 0 like Java if (x < 0) x = -x; if (y < 0) y = -y; if (z < 0) z = -z; int xi = (int)x, yi = (int)y, zi = (int)z; float xf = x - xi, yf = y - yi, zf = z - zi; float r = 0.0f, ampl = 0.5f; for (int oct = 0; oct < noiseOctaves; oct++) { int of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB); float rxf = noise_fsc(xf), ryf = noise_fsc(yf); float n1 = perlinTable[of & PERLIN_SIZE]; n1 += rxf * (perlinTable[(of+1) & PERLIN_SIZE] - n1); float n2 = perlinTable[(of + PERLIN_YWRAP) & PERLIN_SIZE]; n2 += rxf * (perlinTable[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2); n1 += ryf * (n2 - n1); of += PERLIN_ZWRAP; n2 = perlinTable[of & PERLIN_SIZE]; n2 += rxf * (perlinTable[(of+1) & PERLIN_SIZE] - n2); float n3 = perlinTable[(of + PERLIN_YWRAP) & PERLIN_SIZE]; n3 += rxf * (perlinTable[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3); n2 += ryf * (n3 - n2); n1 += noise_fsc(zf) * (n2 - n1); r += n1 * ampl; ampl *= noiseFalloff; // Double frequency each octave xi <<= 1; xf *= 2.0f; if (xf >= 1.0f) { xi++; xf--; } yi <<= 1; yf *= 2.0f; if (yf >= 1.0f) { yi++; yf--; } zi <<= 1; zf *= 2.0f; if (zf >= 1.0f) { zi++; zf--; } } return r; } float PApplet::noise(float x) { return noise(x, 0.0f, 0.0f); } float PApplet::noise(float x, float y) { return noise(x, y, 0.0f); } // Seeded random -- Mersenne Twister for reproducibility matching Java Processing void PApplet::randomSeed(long s) { _rng.seed(static_cast(s)); _rngDist.reset(); } float PApplet::random(float lo, float hi) { return lo + _rngDist(_rng) * (hi - lo); } float PApplet::random(float hi) { return random(0.f, hi); } float PApplet::randomGaussian(){ static float spare; static bool has=false; if(has){has=false;return spare;} float u,v,s; do{u=_rngDist(_rng)*2-1;v=_rngDist(_rng)*2-1;s=u*u+v*v;}while(s>=1||s==0); s=std::sqrt(-2*std::log(s)/s);spare=v*s;has=true;return u*s; } // ============================================================================= // COLOR MODE & HELPERS // ============================================================================= void PApplet::hsbToRgb(float h, float s, float b, float& outR, float& outG, float& outB) { // Normalise each channel to [0, 1] h /= colorMaxH; s /= colorMaxS; b /= colorMaxB; if (s == 0.0f) { // Achromatic (grey): all channels equal brightness outR = outG = outB = b; return; } float sector = h * 6.0f; // which 60-degree sector of the hue wheel int i = (int)sector; float frac = sector - i; // fractional part within sector float p = b * (1.0f - s); float q = b * (1.0f - s * frac); float t = b * (1.0f - s * (1.0f - frac)); switch (i % 6) { case 0: outR = b; outG = t; outB = p; break; case 1: outR = q; outG = b; outB = p; break; case 2: outR = p; outG = b; outB = t; break; case 3: outR = p; outG = q; outB = b; break; case 4: outR = t; outG = p; outB = b; break; default: outR = b; outG = p; outB = q; break; } } color PApplet::makeColor(float a, float b, float c, float d) { float r = 0, g = 0, bv = 0, aa = 0; if (colorModeVal == HSB) { hsbToRgb(a, b, c, r, g, bv); aa = d / colorMaxA; } else { r = a / colorMaxH; g = b / colorMaxS; bv = c / colorMaxB; aa = d / colorMaxA; } return colorVal((int)(r*255), (int)(g*255), (int)(bv*255), (int)(aa*255)); } color PApplet::makeColor(float gray,float alpha){ // In HSB mode, a single-value gray maps to brightness only (hue=0, sat=0) // matching Processing Java behavior -- background(v) in HSB gives gray if(colorModeVal==HSB){ float br=gray/colorMaxB; br=std::fmax(0.f,std::fmin(1.f,br)); int v=(int)(br*255); unsigned int a=std::fmax(0.f,std::fmin(1.f,alpha/colorMaxA))*255; return colorVal(v,v,v,(int)a); } return makeColor(gray,gray,gray,alpha); } // ============================================================================= // color STRUCT CONSTRUCTORS // ============================================================================= // File-scope shims so color:: constructors can call makeColor static color _makeColor(float a,float b,float c,float d=255){ if(PApplet::g_papplet) return PApplet::g_papplet->makeColor(a,b,c,d); return colorVal((int)a,(int)b,(int)c,(int)d); } static color _makeColor(float gray,float alpha=255){ if(PApplet::g_papplet) return PApplet::g_papplet->makeColor(gray,alpha); return colorVal((int)gray,(int)gray,(int)gray,(int)alpha); } static float _colorMaxA(){ return PApplet::g_papplet ? PApplet::g_papplet->colorMaxA : 255.f; } color::color(int gray) { value = _makeColor((float)gray, _colorMaxA()).value; } color::color(int gray, int a) { value = _makeColor((float)gray, (float)a).value; } color::color(int r, int g, int b) { value = _makeColor((float)r, (float)g, (float)b, _colorMaxA()).value; } color::color(int r, int g, int b, int a) { value = _makeColor((float)r, (float)g, (float)b, (float)a).value; } color::color(float gray) { value = _makeColor(gray, _colorMaxA()).value; } color::color(float gray, float a) { value = _makeColor(gray, a).value; } color::color(float r,float g,float b){ value = _makeColor(r,g,b,_colorMaxA()).value; } color::color(float r,float g,float b,float a){ value = _makeColor(r,g,b,a).value; } void PApplet::colorMode(int mode, float mx) { colorModeVal=mode; colorMaxH=colorMaxS=colorMaxB=colorMaxA=mx; } void PApplet::colorMode(int mode, float mH, float mS, float mB, float mA) { colorModeVal=mode; colorMaxH=mH; colorMaxS=mS; colorMaxB=mB; colorMaxA=mA; } // Color channel accessors -- scaled to current colorMode range float PApplet::red(color c) { unsigned int v=c.value; return (v>>16&0xFF)/255.0f*colorMaxH; } float PApplet::green(color c) { unsigned int v=c.value; return (v>>8&0xFF)/255.0f*colorMaxS; } float PApplet::blue(color c) { unsigned int v=c.value; return (v&0xFF)/255.0f*colorMaxB; } float PApplet::alpha(color c) { unsigned int v=c.value; return (v>>24&0xFF)/255.0f*colorMaxA; } float PApplet::brightness(color c) { unsigned int v=c.value; float r=(v>>16&0xFF)/255.f, g=(v>>8&0xFF)/255.f, b=(v&0xFF)/255.f; return max(r, max(g, b)) * colorMaxB; } float PApplet::saturation(color c) { unsigned int v = c.value; float r = (v >> 16 & 0xFF) / 255.f; float g = (v >> 8 & 0xFF) / 255.f; float b = (v & 0xFF) / 255.f; float mx = max(r, max(g, b)); float mn = min(r, min(g, b)); return (mx == 0 ? 0 : (mx - mn) / mx) * colorMaxS; } float PApplet::hue(color c){ unsigned int v=c.value; float r=(v>>16&0xFF)/255.f,g=(v>>8&0xFF)/255.f,b=(v&0xFF)/255.f; float mx=max(r,max(g,b)),mn=min(r,min(g,b)),d=mx-mn; if(d==0)return 0; float h=(mx==r)?(g-b)/d:(mx==g)?2+(b-r)/d:4+(r-g)/d; h*=60;if(h<0)h+=360;return h/360.0f*colorMaxH; } color PApplet::lerpColor(color c1, color c2, float t) { unsigned int v1 = c1.value; unsigned int v2 = c2.value; int r1 = (v1 >> 16) & 0xFF, r2 = (v2 >> 16) & 0xFF; int g1 = (v1 >> 8) & 0xFF, g2 = (v2 >> 8) & 0xFF; int b1 = v1 & 0xFF, b2 = v2 & 0xFF; int a1 = (v1 >> 24) & 0xFF, a2 = (v2 >> 24) & 0xFF; return colorVal( (int)(r1 + t * (r2 - r1)), (int)(g1 + t * (g2 - g1)), (int)(b1 + t * (b2 - b1)), (int)(a1 + t * (a2 - a1)) ); } // ============================================================================= // INTERNAL HELPERS // ============================================================================= void PApplet::applyFill() { glColor4f(fillR, fillG, fillB, fillA); // Always enable blend -- shapes with alpha need it, opaque shapes don't hurt glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } // Temporarily suspend lighting so stroke lines/points render with their exact // colour. Processing Java does the same -- strokes are never affected by lights. void PApplet::applyStroke() { if (lightsEnabled) { glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); } glColor4f(strokeR, strokeG, strokeB, strokeA); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } // Restore lighting after a stroke draw call. void PApplet::restoreLighting() { if (lightsEnabled) { glEnable(GL_LIGHTING); glEnable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE); // Reapply fill colour so the next lit shape uses the right material glColor4f(fillR, fillG, fillB, fillA); } } void PApplet::initPersistFBO(){ if(persistFBO){glDeleteFramebuffers(1,&persistFBO);glDeleteTextures(1,&persistTex);persistFBO=0;} glGenFramebuffers(1,&persistFBO); glGenTextures(1,&persistTex); glBindTexture(GL_TEXTURE_2D,persistTex); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,fbW,fbH,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glBindTexture(GL_TEXTURE_2D,0); glBindFramebuffer(GL_FRAMEBUFFER,persistFBO); glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,persistTex,0); glBindFramebuffer(GL_FRAMEBUFFER,0); } // Copy current back buffer into persist FBO void PApplet::saveToPersist(){ if(!persistFBO) initPersistFBO(); // Blit back buffer -> persist FBO glBindFramebuffer(GL_READ_FRAMEBUFFER,0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER,persistFBO); glBlitFramebuffer(0,0,fbW,fbH,0,0,fbW,fbH,GL_COLOR_BUFFER_BIT,GL_NEAREST); glBindFramebuffer(GL_FRAMEBUFFER,0); } // Restore persist FBO into back buffer void PApplet::restoreFromPersist(){ if(!persistFBO) return; glBindFramebuffer(GL_READ_FRAMEBUFFER,persistFBO); glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0); glBlitFramebuffer(0,0,fbW,fbH,0,0,fbW,fbH,GL_COLOR_BUFFER_BIT,GL_NEAREST); glBindFramebuffer(GL_FRAMEBUFFER,0); } void PApplet::_restoreMainCanvas(){ // Restore main window viewport and projection after PGraphics endDraw() glViewport(0,0,fbW,fbH); glMatrixMode(GL_PROJECTION);glLoadIdentity(); glOrtho(0,logicalW,logicalH,0,-1,1); glMatrixMode(GL_MODELVIEW);glLoadIdentity(); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); } void PApplet::setProjection(int,int){ // Viewport = actual framebuffer size (handles HiDPI where fb > logical). // Ortho = logical sketch size (coordinates always match what size() set). if (gWindow) { int fw=logicalW, fh=logicalH; glfwGetFramebufferSize(gWindow, &fw, &fh); if (fw > 0) fbW = fw; if (fh > 0) fbH = fh; } glViewport(0, 0, fbW, fbH); glMatrixMode(GL_PROJECTION);glLoadIdentity(); glOrtho(0, logicalW, logicalH, 0, -1, 1); glMatrixMode(GL_MODELVIEW);glLoadIdentity(); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); } void PApplet::drawEllipseGeom(float cx,float cy,float rx,float ry, float sa,float ea,int segs){ if(segs < 0){ float maxR = rx > ry ? rx : ry; segs = (int)(maxR * 2.5f); if(segs < 64) segs = 64; if(segs > 512) segs = 512; } float range=ea-sa; bool isFullCircle = (std::fabs(range) >= TWO_PI - 0.001f); if(doFill){ applyFill(); glBegin(GL_TRIANGLE_FAN); glVertex2f(cx,cy); // center for(int i=0;i<=segs;i++){ float a=sa+range*i/segs; glVertex2f(cx+rx*std::cos(a),cy+ry*std::sin(a)); } if(isFullCircle){ // close the circle back to the first arc point glVertex2f(cx+rx*std::cos(sa),cy+ry*std::sin(sa)); } glEnd(); } if(doStroke){ applyStroke();glLineWidth(strokeW); if(isFullCircle){ glBegin(GL_LINE_LOOP); } else { glBegin(GL_LINE_STRIP); } for(int i=0;i<=segs;i++){ float a=sa+range*i/segs; glVertex2f(cx+rx*std::cos(a),cy+ry*std::sin(a)); } glEnd(); } } void PApplet::resolveRect(float& x,float& y,float& w,float& h){ if(currentRectMode==CENTER){x-=w*0.5f;y-=h*0.5f;} else if(currentRectMode==RADIUS){x-=w;y-=h;w*=2;h*=2;} else if(currentRectMode==CORNERS){w=w-x;h=h-y;} } void PApplet::resolveEllipse(float& cx,float& cy,float& rx,float& ry){ if(currentEllipseMode==CORNER){cx+=rx;cy+=ry;} else if(currentEllipseMode==CORNERS){float ex=rx,ey=ry;rx=(ex-cx)*0.5f;ry=(ey-cy)*0.5f;cx=(cx+ex)*0.5f;cy=(cy+ey)*0.5f;} else if(currentEllipseMode==CENTER){rx*=0.5f;ry*=0.5f;} } void PApplet::setFillFromColor(color c) { unsigned int v = c.value; fillR = (v >> 16 & 0xFF) / 255.f; fillG = (v >> 8 & 0xFF) / 255.f; fillB = (v & 0xFF) / 255.f; fillA = (v >> 24 & 0xFF) / 255.f; doFill = true; } void PApplet::setStrokeFromColor(color c) { unsigned int v = c.value; strokeR = (v >> 16 & 0xFF) / 255.f; strokeG = (v >> 8 & 0xFF) / 255.f; strokeB = (v & 0xFF) / 255.f; strokeA = (v >> 24 & 0xFF) / 255.f; doStroke = true; } // ============================================================================= // ENVIRONMENT // ============================================================================= void PApplet::size(int w,int h){ winWidth=w;winHeight=h; logicalW=w;logicalH=h; // remember what the sketch requested pixelWidth=w;pixelHeight=h; if(gWindow){ glfwSetWindowSize(gWindow,w,h); // Poll until the framebuffer is actually the requested size. // glfwSetWindowSize is async; without this the window stays 100x100 // when setup() calls background() or draws anything. // Poll until the window is actually resized (or timeout after 500ms) for(int _wait=0; _wait<500; _wait++){ glfwPollEvents(); int fw=0,fh=0; glfwGetFramebufferSize(gWindow,&fw,&fh); // Accept exact match or close match (HiDPI scaling) if(fw>0 && fh>0){ pixelWidth=fw; pixelHeight=fh; // Check if the logical window size matches what we requested int lw=0,lh=0; glfwGetWindowSize(gWindow,&lw,&lh); if(lw==w && lh==h) break; // exact match } if(_wait>=50){ // after 50ms, accept whatever we have int fw2=0,fh2=0; glfwGetFramebufferSize(gWindow,&fw2,&fh2); if(fw2>0&&fh2>0){ pixelWidth=fw2; pixelHeight=fh2; } break; } #ifdef _WIN32 Sleep(1); #else usleep(1000); #endif } // Force coordinate system to requested size regardless of WM. // logicalW/H are the sketch's coordinate space; viewport uses actual size. logicalW = w; logicalH = h; winWidth = w; winHeight = h; setProjection(w,h); } } void PApplet::size(int w,int h,int renderer){ defaultP3D=(renderer==P3D); size(w,h); // For P3D mode: set up depth test and apply default camera/perspective // immediately so setup() draws with the correct projection. if(defaultP3D && gWindow){ {int fw=logicalW,fh=logicalH;if(gWindow)glfwGetFramebufferSize(gWindow,&fw,&fh);if(fw>0)fbW=fw;if(fh>0)fbH=fh;} glViewport(0,0,fbW,fbH); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glDisable(GL_CULL_FACE); glFrontFace(GL_CW); glEnable(GL_NORMALIZE); glClearColor(0.8f,0.8f,0.8f,1); // Java Processing default grey (204,204,204) glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); applyDefaultCamera(); } } void PApplet::fullScreen() { if (!gWindow) { winWidth = displayWidth; winHeight = displayHeight; } else { GLFWmonitor* m = glfwGetPrimaryMonitor(); const GLFWvidmode* v = glfwGetVideoMode(m); glfwSetWindowMonitor(gWindow, m, 0, 0, v->width, v->height, v->refreshRate); } } void PApplet::frameRate(int fps){ targetFrameTime = 1.0/fps; } void PApplet::noLoop(){looping=false;} void PApplet::loop() {looping=true;} void PApplet::redraw(){redrawOnce=true;} void PApplet::exit_sketch(){if(gWindow)glfwSetWindowShouldClose(gWindow,GLFW_TRUE);} void PApplet::windowTitle(const std::string& t){if(gWindow)glfwSetWindowTitle(gWindow,t.c_str());} void PApplet::windowMove(int x,int y){if(gWindow)glfwSetWindowPos(gWindow,x,y);} void PApplet::windowResize(int w,int h){size(w,h);} void PApplet::windowResizable(bool r){isResizable=r;if(gWindow)glfwSetWindowAttrib(gWindow,GLFW_RESIZABLE,r?GLFW_TRUE:GLFW_FALSE);} // --------------------------------------------------------------------------- // Clipboard // --------------------------------------------------------------------------- void PApplet::setClipboard(const std::string& s) { if (s.empty()) return; if (gWindow) glfwSetClipboardString(gWindow, s.c_str()); } std::string PApplet::getClipboard() { if (!gWindow) return ""; const char* cb = glfwGetClipboardString(gWindow); return cb ? std::string(cb) : ""; } // --------------------------------------------------------------------------- // Window icon // --------------------------------------------------------------------------- void PApplet::setWindowIcon(PImage* img) { if (!img || !gWindow) return; // Convert ARGB pixels (Processing internal) to RGBA (GLFW wants RGBA) std::vector rgba(img->width * img->height * 4); for (int p = 0; p < img->width * img->height; p++) { unsigned int px = img->pixels[p]; rgba[p*4+0] = (px >> 16) & 0xFF; // R rgba[p*4+1] = (px >> 8) & 0xFF; // G rgba[p*4+2] = (px ) & 0xFF; // B rgba[p*4+3] = (px >> 24) & 0xFF; // A } GLFWimage gi; gi.width = img->width; gi.height = img->height; gi.pixels = rgba.data(); glfwSetWindowIcon(gWindow, 1, &gi); } // --------------------------------------------------------------------------- // Modifier key state // --------------------------------------------------------------------------- bool PApplet::isCtrlDown() { // Use GLFW mods bitmask (reliable from callbacks) OR glfwGetKey (for polling) if (g_currentMods & GLFW_MOD_CONTROL) return true; if (!gWindow) return false; return glfwGetKey(gWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(gWindow, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS; } bool PApplet::isShiftDown() { if (g_currentMods & GLFW_MOD_SHIFT) return true; if (!gWindow) return false; return glfwGetKey(gWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(gWindow, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS; } bool PApplet::isAltDown() { if (g_currentMods & GLFW_MOD_ALT) return true; if (!gWindow) return false; return glfwGetKey(gWindow, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(gWindow, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS; } void PApplet::windowRatio(int w,int h){if(gWindow)glfwSetWindowAspectRatio(gWindow,w,h);} void PApplet::pixelDensity(int d){pixelDensityValue=d;} void PApplet::smooth(int level) { // The MAIN window's actual MSAA sample count is fixed at window-creation // time (GLFW_SAMPLES is a window hint, can't change after the window // exists) -- so smooth(level) can't retroactively change the main // canvas's hardware sample count the way real Processing's P2D/P3D // renderers can. What we CAN do: store the requested level (used by // PGraphics buffers below, which DO get to choose their own sample // count fresh at creation time), and toggle the same smoothing flags // smooth() always has, regardless of which level was requested. smoothing = true; smoothLevel = level; glEnable(GL_LINE_SMOOTH);glHint(GL_LINE_SMOOTH_HINT,GL_NICEST); glEnable(GL_POINT_SMOOTH);glHint(GL_POINT_SMOOTH_HINT,GL_NICEST); glEnable(GL_MULTISAMPLE); } void PApplet::noSmooth(){smoothing=false;glDisable(GL_LINE_SMOOTH);glDisable(GL_POLYGON_SMOOTH);glDisable(GL_POINT_SMOOTH);glDisable(GL_MULTISAMPLE);} void PApplet::hint(int which){ switch(which){ case ENABLE_DEPTH_TEST: glEnable(GL_DEPTH_TEST); break; case DISABLE_DEPTH_TEST: glDisable(GL_DEPTH_TEST); break; case ENABLE_DEPTH_SORT: glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL); break; case DISABLE_DEPTH_SORT: glDepthFunc(GL_LESS); break; default: break; } } void PApplet::cursor() {if(gWindow)glfwSetInputMode(gWindow,GLFW_CURSOR,GLFW_CURSOR_NORMAL);} void PApplet::cursor(int type){if(!gWindow)return;GLFWcursor* c=glfwCreateStandardCursor(type);if(c)glfwSetCursor(gWindow,c);} void PApplet::noCursor() {if(gWindow)glfwSetInputMode(gWindow,GLFW_CURSOR,GLFW_CURSOR_HIDDEN);} // captureMouse(): locks cursor to window and provides unlimited delta movement. // Use releaseMouse() or press ESC to free it. void PApplet::captureMouse() {if(gWindow){glfwSetInputMode(gWindow,GLFW_CURSOR,GLFW_CURSOR_DISABLED); // Enable raw motion if supported (removes OS acceleration) if(glfwRawMouseMotionSupported()) glfwSetInputMode(gWindow,GLFW_RAW_MOUSE_MOTION,GLFW_TRUE);}} void PApplet::releaseMouse(){if(gWindow){glfwSetInputMode(gWindow,GLFW_CURSOR,GLFW_CURSOR_NORMAL); glfwSetInputMode(gWindow,GLFW_RAW_MOUSE_MOTION,GLFW_FALSE);}} // ============================================================================= // STYLE STACK // ============================================================================= void PApplet::captureStyle(Style& s) { s = { fillR, fillG, fillB, fillA, strokeR, strokeG, strokeB, strokeA, strokeW, doFill, doStroke, currentRectMode, currentEllipseMode, currentImageMode, tintR, tintG, tintB, tintA, doTint, colorModeVal, colorMaxH, colorMaxS, colorMaxB, colorMaxA }; } void PApplet::restoreStyle(const Style& s) { fillR = s.fillR; fillG = s.fillG; fillB = s.fillB; fillA = s.fillA; strokeR = s.strokeR; strokeG = s.strokeG; strokeB = s.strokeB; strokeA = s.strokeA; strokeW = s.strokeW; doFill = s.doFill; doStroke = s.doStroke; currentRectMode = s.rectMode; currentEllipseMode = s.ellipseMode; currentImageMode = s.imageMode; tintR = s.tintR; tintG = s.tintG; tintB = s.tintB; tintA = s.tintA; doTint = s.doTint; colorModeVal = s.colorMode; colorMaxH = s.cmH; colorMaxS = s.cmS; colorMaxB = s.cmB; colorMaxA = s.cmA; } void PApplet::pushStyle() { Style s; captureStyle(s); styleStack.push_back(s); } void PApplet::popStyle() { if (!styleStack.empty()) { restoreStyle(styleStack.back()); styleStack.pop_back(); } } void PApplet::push() { glPushMatrix(); pushStyle(); } void PApplet::pop() { glPopMatrix(); popStyle(); } void PApplet::pushMatrix() { glPushMatrix(); } void PApplet::popMatrix() { glPopMatrix(); } // ============================================================================= // BACKGROUND / CLEAR // ============================================================================= void PApplet::setBg(float r,float g,float b,float a){ bgR=r;bgG=g;bgB=b;bgA=a; glClearColor(r,g,b,a); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); } void PApplet::background(float gray, float a) { _backgroundCalledThisFrame = true; color c = makeColor(gray, a); unsigned int v = c.value; setBg((v>>16&0xFF)/255.f, (v>>8&0xFF)/255.f, (v&0xFF)/255.f, (v>>24&0xFF)/255.f); } void PApplet::background(float r, float g, float b, float a) { _backgroundCalledThisFrame = true; color c = makeColor(r, g, b, a); unsigned int v = c.value; setBg((v>>16&0xFF)/255.f, (v>>8&0xFF)/255.f, (v&0xFF)/255.f, (v>>24&0xFF)/255.f); } void PApplet::background(color c) { _backgroundCalledThisFrame = true; unsigned int v = c.value; setBg((v>>16&0xFF)/255.f, (v>>8&0xFF)/255.f, (v&0xFF)/255.f, (v>>24&0xFF)/255.f); } void PApplet::background(const PImage& img) { _backgroundCalledThisFrame = true; // Draw image as full-canvas background if (img.width == 0 || img.height == 0) return; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, logicalW, logicalH, 0, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glDisable(GL_DEPTH_TEST); drawImageRect(const_cast(img), 0, 0, (float)logicalW, (float)logicalH); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); if (defaultP3D) glEnable(GL_DEPTH_TEST); } void PApplet::clear(){glClearColor(0,0,0,0);glClear(GL_COLOR_BUFFER_BIT);} // ============================================================================= // FILL / STROKE // ============================================================================= void PApplet::fill(float gray,float a) {setFillFromColor(makeColor(gray,a));} void PApplet::fill(float gray) {setFillFromColor(makeColor(gray,colorMaxA));} void PApplet::fill(float r,float g,float b,float a) {setFillFromColor(makeColor(r,g,b,a));} void PApplet::fill(float r,float g,float b) {setFillFromColor(makeColor(r,g,b,colorMaxA));} void PApplet::fill(color c) {setFillFromColor(c);} void PApplet::fill(color c, float a) { unsigned int v = c.value; fillR = (v>>16&0xFF)/255.f; fillG = (v>>8 &0xFF)/255.f; fillB = (v &0xFF)/255.f; fillA = std::min(255.f, std::max(0.f, a)) / 255.f; doFill = true; } void PApplet::noFill() {doFill=false;} void PApplet::stroke(float gray,float a) {setStrokeFromColor(makeColor(gray,a));} void PApplet::stroke(float gray) {setStrokeFromColor(makeColor(gray,colorMaxA));} void PApplet::stroke(float r,float g,float b,float a){setStrokeFromColor(makeColor(r,g,b,a));} void PApplet::stroke(float r,float g,float b) {setStrokeFromColor(makeColor(r,g,b,colorMaxA));} void PApplet::stroke(color c) {setStrokeFromColor(c);} void PApplet::noStroke() {doStroke=false;} void PApplet::strokeWeight(float w) {strokeW=w;} void PApplet::strokeCap(int) {} void PApplet::strokeJoin(int) {} // ============================================================================= // PCOLOR CONVENIENCE OVERLOADS // ============================================================================= void PApplet::fill(const PColor& c) { fill(c.r, c.g, c.b, c.a); } void PApplet::stroke(const PColor& c) { stroke(c.r, c.g, c.b, c.a); } void PApplet::background(const PColor& c){ background(c.r, c.g, c.b, c.a); } void PApplet::tint(const PColor& c) { tint(c.r, c.g, c.b, c.a); } void PApplet::rectMode(int m) {currentRectMode=m;} void PApplet::ellipseMode(int m) {currentEllipseMode=m;} // ============================================================================= // 2D PRIMITIVES // ============================================================================= static void flushPoints(){} // no-op, points drawn immediately now void PApplet::point(float x, float y) { if (!doStroke) return; applyStroke(); if (!smoothing && strokeW <= 1.0f) { glPointSize(1.0f); glBegin(GL_POINTS); glVertex2f(std::floor(x)+0.5f, std::floor(y)+0.5f); glEnd(); } else { glPointSize(strokeW); glBegin(GL_POINTS); glVertex2f(x, y); glEnd(); } restoreLighting(); } void PApplet::point(float x, float y, float z) { if (!doStroke) return; applyStroke(); glPointSize(strokeW); glBegin(GL_POINTS); glVertex3f(x, y, z); glEnd(); restoreLighting(); } void PApplet::line(float x1, float y1, float x2, float y2) { { GLint mvDepth=0, projDepth=0; glGetIntegerv(GL_MODELVIEW_STACK_DEPTH, &mvDepth); glGetIntegerv(GL_PROJECTION_STACK_DEPTH, &projDepth); double mv[16], proj[16]; glGetDoublev(GL_MODELVIEW_MATRIX, mv); glGetDoublev(GL_PROJECTION_MATRIX, proj); GLint vp[4]; glGetIntegerv(GL_VIEWPORT, vp); GLint curFbo=0; glGetIntegerv(GL_FRAMEBUFFER_BINDING, &curFbo); PDEBUG("PApplet::line(%.1f,%.1f,%.1f,%.1f) ENTRY: mvDepth=%d projDepth=%d curFbo=%d viewport=(%d,%d,%d,%d)\n", x1,y1,x2,y2, mvDepth, projDepth, curFbo, vp[0],vp[1],vp[2],vp[3]); PDEBUG(" mv[0]=%.3f mv[5]=%.3f mv[12]=%.3f mv[13]=%.3f\n", mv[0],mv[5],mv[12],mv[13]); PDEBUG(" proj[0]=%.6f proj[5]=%.6f proj[12]=%.6f proj[13]=%.6f\n", proj[0],proj[5],proj[12],proj[13]); } if (!doStroke) return; applyStroke(); float w = strokeW; if (w <= 1.0f) { // Thin lines: use GL_LINES glLineWidth(1.0f); glBegin(GL_LINES); glVertex2f(x1,y1); glVertex2f(x2,y2); glEnd(); restoreLighting(); return; } // Thick line: quad body + round caps using stencil to prevent double-blend float dx = x2-x1, dy = y2-y1; float len = std::sqrt(dx*dx+dy*dy); if(len < 0.0001f){ restoreLighting(); return; } float ux = dx/len, uy = dy/len; float r = w*0.5f; float px2 = -uy*r, py2 = ux*r; // perpendicular int segs = std::max(16, (int)(r*4.0f)); // Use stencil to draw all geometry, then fill once -- prevents double-blend glEnable(GL_STENCIL_TEST); glClearStencil(0); glClear(GL_STENCIL_BUFFER_BIT); glStencilFunc(GL_ALWAYS, 1, 0xFF); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE); // Draw body into stencil glBegin(GL_QUADS); glVertex2f(x1+px2,y1+py2); glVertex2f(x2+px2,y2+py2); glVertex2f(x2-px2,y2-py2); glVertex2f(x1-px2,y1-py2); glEnd(); // Draw end caps into stencil for(int ep=0;ep<2;ep++){ float cx2=ep?x2:x1, cy2=ep?y2:y1; glBegin(GL_TRIANGLE_FAN); glVertex2f(cx2,cy2); for(int s=0;s<=segs;s++){ float a=s*TWO_PI/segs; glVertex2f(cx2+std::cos(a)*r, cy2+std::sin(a)*r); } glEnd(); } // Now draw color only where stencil=1 glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); glStencilFunc(GL_EQUAL, 1, 0xFF); glStencilOp(GL_KEEP,GL_KEEP,GL_KEEP); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); // Fill bounding box with stroke color float minx=std::min(x1,x2)-r, maxx=std::max(x1,x2)+r; float miny=std::min(y1,y2)-r, maxy=std::max(y1,y2)+r; glBegin(GL_QUADS); glVertex2f(minx,miny); glVertex2f(maxx,miny); glVertex2f(maxx,maxy); glVertex2f(minx,maxy); glEnd(); glDisable(GL_STENCIL_TEST); restoreLighting(); } void PApplet::line(float x1, float y1, float z1, float x2, float y2, float z2) { if (!doStroke) return; applyStroke(); glLineWidth(strokeW); glBegin(GL_LINES); glVertex3f(x1,y1,z1); glVertex3f(x2,y2,z2); glEnd(); restoreLighting(); } void PApplet::ellipse(float cx, float cy, float w, float h) { float rx = w, ry = h; resolveEllipse(cx, cy, rx, ry); drawEllipseGeom(cx, cy, rx, ry); } void PApplet::circle(float cx, float cy, float diameter) { ellipse(cx, cy, diameter, diameter); } void PApplet::arc(float cx, float cy, float w, float h, float startAngle, float endAngle) { float rx = w, ry = h; resolveEllipse(cx, cy, rx, ry); drawEllipseGeom(cx, cy, rx, ry, startAngle, endAngle); } void PApplet::arc(float cx, float cy, float w, float h, float startAngle, float endAngle, int /*mode*/) { // mode = OPEN / CHORD / PIE -- basic implementation uses OPEN arc(cx, cy, w, h, startAngle, endAngle); } void PApplet::rect(float x, float y, float w, float h) { resolveRect(x, y, w, h); if (doFill) { applyFill(); glBegin(GL_QUADS); glVertex2f(x, y ); glVertex2f(x + w, y ); glVertex2f(x + w, y + h); glVertex2f(x, y + h); glEnd(); } if (doStroke) { // Real Processing centers the stroke ON the rectangle's boundary // (half the weight extends inward over the fill, half extends // outward) -- NOT entirely outside it. The previous version drew // the stroke loop expanded fully outward, which meant increasing // strokeWeight never visually shrank the filled interior (it // does in real Processing, since the inward half of a thick // stroke covers part of the fill), and glLineWidth-rendered // GL_LINE_LOOP gives flat, unmitered corners at large widths // (visible gaps/seams at corners instead of a clean joined // outline). Drawing the stroke as actual filled quad geometry -- // matching real Processing's approach -- fixes both: correct // centering AND clean mitered corners via overlapping quads. applyStroke(); float half = strokeW * 0.5f; float xo = x - half, yo = y - half; // outer edge float xi = x + half, yi = y + half; // inner edge (top-left side) float xow = x + w + half, yoh = y + h + half; // outer edge (bottom-right side) float xiw = x + w - half, yih = y + h - half; // inner edge (bottom-right side) glBegin(GL_QUADS); // Top edge glVertex2f(xo, yo); glVertex2f(xow, yo); glVertex2f(xow, yi); glVertex2f(xo, yi); // Bottom edge glVertex2f(xo, yih); glVertex2f(xow, yih); glVertex2f(xow, yoh); glVertex2f(xo, yoh); // Left edge (between top and bottom strips already drawn) glVertex2f(xo, yi); glVertex2f(xi, yi); glVertex2f(xi, yih); glVertex2f(xo, yih); // Right edge glVertex2f(xiw, yi); glVertex2f(xow, yi); glVertex2f(xow, yih); glVertex2f(xiw, yih); glEnd(); restoreLighting(); } } void PApplet::rect(float x, float y, float w, float h, float r) { resolveRect(x, y, w, h); // Clamp radius so it never exceeds half the shortest side r = min(r, min(w, h) * 0.5f); const int cornerSegs = 8; // segments per 90-degree corner arc // Emit a corner arc: center (cx,cy), from angle sa to ea auto corner = [&](float cx, float cy, float sa, float ea) { for (int i = 0; i <= cornerSegs; i++) { float a = sa + (ea - sa) * i / cornerSegs; glVertex2f(cx + r * std::cos(a), cy + r * std::sin(a)); } }; if (doFill) { applyFill(); glBegin(GL_TRIANGLE_FAN); // Fan center at rect midpoint glVertex2f(x + w * 0.5f, y + h * 0.5f); // Four corners in CCW order, closing back to the first arc point corner(x + r, y + r, PI, PI + HALF_PI); // top-left corner(x + w - r, y + r, PI + HALF_PI, TWO_PI ); // top-right corner(x + w - r, y + h - r, 0, HALF_PI ); // bottom-right corner(x + r, y + h - r, HALF_PI, PI ); // bottom-left // Close the fan back to the first arc point (avoids gap/seam) glVertex2f(x + r + r * std::cos(PI), y + r + r * std::sin(PI)); glEnd(); } if (doStroke) { applyStroke(); glLineWidth(strokeW); glBegin(GL_LINE_LOOP); corner(x + r, y + r, PI, PI + HALF_PI); corner(x + w - r, y + r, PI + HALF_PI, TWO_PI ); corner(x + w - r, y + h - r, 0, HALF_PI ); corner(x + r, y + h - r, HALF_PI, PI ); glEnd(); } } void PApplet::square(float x,float y,float s){rect(x,y,s,s);} void PApplet::triangle(float x1, float y1, float x2, float y2, float x3, float y3) { if (doFill) { applyFill(); glBegin(GL_TRIANGLES); glVertex2f(x1, y1); glVertex2f(x2, y2); glVertex2f(x3, y3); glEnd(); } if (doStroke) { applyStroke(); glLineWidth(strokeW); glBegin(GL_LINE_LOOP); glVertex2f(x1, y1); glVertex2f(x2, y2); glVertex2f(x3, y3); glEnd(); restoreLighting(); } } void PApplet::quad(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { if (doFill) { applyFill(); glBegin(GL_QUADS); glVertex2f(x1, y1); glVertex2f(x2, y2); glVertex2f(x3, y3); glVertex2f(x4, y4); glEnd(); } if (doStroke) { applyStroke(); glLineWidth(strokeW); glBegin(GL_LINE_LOOP); glVertex2f(x1, y1); glVertex2f(x2, y2); glVertex2f(x3, y3); glVertex2f(x4, y4); glEnd(); restoreLighting(); } } // ============================================================================= // 3D PRIMITIVES // ============================================================================= void PApplet::rotateX(float a){glRotatef(a*180.0f/PI,1,0,0);} void PApplet::rotateY(float a){glRotatef(a*180.0f/PI,0,1,0);} void PApplet::rotateZ(float a){glRotatef(a*180.0f/PI,0,0,1);} void PApplet::sphereDetail(int r){sphereRes=r;} // ============================================================================= // PHONG SHADING SHADER // ============================================================================= static GLuint compileShader(GLenum type, const char* code) { GLuint sh = glCreateShader(type); glShaderSource(sh, 1, &code, nullptr); glCompileShader(sh); GLint ok; glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); if (!ok) { char b[512]; glGetShaderInfoLog(sh,512,nullptr,b); fprintf(stderr,"[sh] %s\n",b); } return sh; } void PApplet::initPhongShader() { if (phongProg) return; // Phong shader using compatibility profile built-ins. // Works on any system that supports our fixed-function GL pipeline. const char* vs = R"VERT( varying vec3 vN; varying vec3 vP; void main(){ // gl_NormalMatrix = inverse-transpose of modelview upper 3x3 vN = gl_NormalMatrix * gl_Normal; vP = vec3(gl_ModelViewMatrix * gl_Vertex); gl_Position = ftransform(); gl_FrontColor = gl_Color; } )VERT"; const char* fs = R"FRAG( varying vec3 vN; varying vec3 vP; uniform int uNumLights; uniform float uLightConc[8]; uniform float uLightCutCos[8]; void main(){ vec3 N = normalize(vN); vec3 V = normalize(-vP); vec3 col = gl_Color.rgb * gl_LightModel.ambient.rgb; for(int i=0;i<8;i++){ if(i>=uNumLights) break; vec4 lp = gl_LightSource[i].position; bool isDir = (lp.w < 0.5); vec3 L = isDir ? normalize(lp.xyz) : normalize(lp.xyz - vP); float spotAtten = 1.0; if(!isDir && uLightCutCos[i] > -0.5){ vec3 sd = normalize(gl_LightSource[i].spotDirection); float sc = dot(L, -sd); if(sc < uLightCutCos[i]) spotAtten = 0.0; else spotAtten = pow(max(sc,0.0), uLightConc[i]); } if(spotAtten < 0.0001) continue; col += gl_Color.rgb * gl_LightSource[i].ambient.rgb; float d = max(dot(N, L), 0.0); col += gl_Color.rgb * gl_LightSource[i].diffuse.rgb * d * spotAtten; vec3 H = normalize(L + V); float nh = max(dot(N, H), 0.0); float sh = pow(nh, gl_FrontMaterial.shininess + 1.0); col += gl_FrontMaterial.specular.rgb * gl_LightSource[i].specular.rgb * sh * spotAtten; } gl_FragColor = vec4(col, gl_Color.a); } )FRAG"; GLuint v=compileShader(GL_VERTEX_SHADER,vs); GLuint f=compileShader(GL_FRAGMENT_SHADER,fs); phongProg=glCreateProgram(); glAttachShader(phongProg,v); glAttachShader(phongProg,f); glLinkProgram(phongProg); GLint ok; glGetProgramiv(phongProg,GL_LINK_STATUS,&ok); if(!ok){char b[512];glGetProgramInfoLog(phongProg,512,nullptr,b); fprintf(stderr,"[sh link] %s\n",b);phongProg=0;} glDeleteShader(v); glDeleteShader(f); } void PApplet::box(float s){box(s,s,s);} void PApplet::box(float bw,float bh,float bd){ { GLint curFbo=0; glGetIntegerv(GL_FRAMEBUFFER_BINDING,&curFbo); GLboolean depthOn = glIsEnabled(GL_DEPTH_TEST); GLint depthFunc=0; glGetIntegerv(GL_DEPTH_FUNC, &depthFunc); GLboolean depthMask; glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask); GLint vp[4]; glGetIntegerv(GL_VIEWPORT, vp); PDEBUG("PApplet::box(%.1f,%.1f,%.1f) ENTRY: doFill=%d fillR=%.3f fillG=%.3f fillB=%.3f curFbo=%d\n", bw,bh,bd, doFill, fillR, fillG, fillB, curFbo); PDEBUG(" depthTest=%d depthFunc=0x%x depthWriteMask=%d viewport=(%d,%d,%d,%d)\n", depthOn, depthFunc, depthMask, vp[0], vp[1], vp[2], vp[3]); } float hw=bw/2,hh=bh/2,hd=bd/2; struct Face{ float nx,ny,nz; float v[4][3]; }; Face faces[]={ { 0, 0, 1,{{-hw,-hh,hd},{hw,-hh,hd},{hw,hh,hd},{-hw,hh,hd}}}, { 0, 0,-1,{{hw,-hh,-hd},{-hw,-hh,-hd},{-hw,hh,-hd},{hw,hh,-hd}}}, {-1, 0, 0,{{-hw,-hh,-hd},{-hw,-hh,hd},{-hw,hh,hd},{-hw,hh,-hd}}}, { 1, 0, 0,{{hw,-hh,hd},{hw,-hh,-hd},{hw,hh,-hd},{hw,hh,hd}}}, { 0,-1, 0,{{-hw,-hh,-hd},{hw,-hh,-hd},{hw,-hh,hd},{-hw,-hh,hd}}}, { 0, 1, 0,{{-hw,hh,hd},{hw,hh,hd},{hw,hh,-hd},{-hw,hh,-hd}}}, }; if(doFill){ applyFill(); glBegin(GL_QUADS); for(auto& f:faces){ glNormal3f(f.nx,f.ny,f.nz); for(auto& v:f.v) glVertex3f(v[0],v[1],v[2]); } glEnd(); { GLenum err = glGetError(); PDEBUG("PApplet::box() AFTER glEnd(): glError=0x%x firstFaceVertex=(%.1f,%.1f,%.1f)\n", err, faces[0].v[0][0], faces[0].v[0][1], faces[0].v[0][2]); } } if(doStroke){ applyStroke();glLineWidth(strokeW); float vx[]={-hw,-hw,hw,hw,-hw,-hw,hw,hw}; float vy[]={-hh,hh,hh,-hh,-hh,hh,hh,-hh}; float vz[]={hd,hd,hd,hd,-hd,-hd,-hd,-hd}; int e[][2]={{0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}}; glBegin(GL_LINES); for(auto& ee:e){glVertex3f(vx[ee[0]],vy[ee[0]],vz[ee[0]]);glVertex3f(vx[ee[1]],vy[ee[1]],vz[ee[1]]);} glEnd(); restoreLighting(); } } void PApplet::sphere(float r){ int stacks=sphereRes, slices=sphereRes; if(doFill){ // Use Phong (per-pixel) shading when lighting is on for smooth highlights bool usePhong = lightsEnabled; if (usePhong) { initPhongShader(); if (phongProg) { glUseProgram(phongProg); GLint loc = glGetUniformLocation(phongProg, "uNumLights"); if (loc >= 0) glUniform1i(loc, lightIndex); GLint locC = glGetUniformLocation(phongProg, "uLightConc"); if (locC >= 0) glUniform1fv(locC, 8, lightConcentration); GLint locK = glGetUniformLocation(phongProg, "uLightCutCos"); if (locK >= 0) glUniform1fv(locK, 8, lightCutoffCos); } else usePhong = false; } applyFill(); for(int i=0;i 0){ GLenum gm; switch(shapeKind){ case POINTS: gm=GL_POINTS; break; case LINES: gm=GL_LINES; break; case TRIANGLES: gm=GL_TRIANGLES; break; case TRIANGLE_FAN: gm=GL_TRIANGLE_FAN; break; case TRIANGLE_STRIP:gm=GL_TRIANGLE_STRIP;break; case QUADS: gm=GL_QUADS; break; case QUAD_STRIP: gm=GL_QUAD_STRIP; break; default: gm=GL_POLYGON; break; } if(doFill && shapeKind != POINTS && shapeKind != LINES){ glEnable(GL_POLYGON_OFFSET_FILL); glPolygonOffset(1.0f, 1.0f); applyFill(); glBegin(gm); for(auto& v : shapeVerts3D) glVertex3f(v[0], v[1], v[2]); glEnd(); glDisable(GL_POLYGON_OFFSET_FILL); } // POINTS and LINES use stroke color drawn directly if(shapeKind == POINTS){ applyStroke(); glPointSize(strokeW); glBegin(GL_POINTS); for(auto& v : shapeVerts3D) glVertex3f(v[0], v[1], v[2]); glEnd(); restoreLighting(); inShape=false; shape3D=false; shapeVerts.clear(); shapeVerts3D.clear(); return; } if(shapeKind == LINES){ applyStroke(); glLineWidth(strokeW); glBegin(GL_LINES); for(auto& v : shapeVerts3D) glVertex3f(v[0], v[1], v[2]); glEnd(); restoreLighting(); inShape=false; shape3D=false; shapeVerts.clear(); shapeVerts3D.clear(); return; } // Draw stroke outlines -- shapeVerts was collected in vertex() if(doStroke && !shapeVerts.empty()){ applyStroke(); glLineWidth(strokeW); int n=(int)shapeVerts.size(); // Use 3D verts for correct depth in transformed space auto lv=[&](int i)->std::array{ return shapeVerts3D[i]; }; switch(shapeKind){ case TRIANGLE_STRIP: glBegin(GL_LINES); for(int i=0;i+2=3){ glDisable(GL_CULL_FACE); // Step 1: Write polygon to stencil buffer using odd-even fill rule glEnable(GL_STENCIL_TEST); glClear(GL_STENCIL_BUFFER_BIT); glStencilFunc(GL_ALWAYS, 0, ~0); glStencilOp(GL_KEEP, GL_KEEP, GL_INVERT); // toggle stencil bit glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // don't write color glBegin(GL_TRIANGLE_FAN); for(auto& v : shapeVerts3D) glVertex3f(v[0], v[1], v[2]); glEnd(); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); // Step 2: Draw fill color only where stencil != 0 glStencilFunc(GL_NOTEQUAL, 0, ~0); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); applyFill(); // Draw a bounding quad covering the shape float minx=shapeVerts3D[0][0], maxx=minx; float miny=shapeVerts3D[0][1], maxy=miny; float minz=shapeVerts3D[0][2], maxz=minz; for(auto& v:shapeVerts3D){ minx=std::min(minx,v[0]); maxx=std::max(maxx,v[0]); miny=std::min(miny,v[1]); maxy=std::max(maxy,v[1]); minz=std::min(minz,v[2]); maxz=std::max(maxz,v[2]); } float mz=(minz+maxz)*0.5f; glBegin(GL_QUADS); glVertex3f(minx,miny,mz); glVertex3f(maxx,miny,mz); glVertex3f(maxx,maxy,mz); glVertex3f(minx,maxy,mz); glEnd(); glDisable(GL_STENCIL_TEST); } if(doStroke){ applyStroke(); glLineWidth(strokeW); glBegin(cl ? GL_LINE_LOOP : GL_LINE_STRIP); // Use 3D coords so transforms (rotateX/Y) are respected for(auto& v : shapeVerts3D) glVertex3f(v[0], v[1], v[2]); glEnd(); restoreLighting(); } } inShape=false; shapeVerts.clear(); shapeVerts3D.clear(); } void PApplet::bezierVertex(float cx1,float cy1,float cx2,float cy2,float x,float y){ if(!inShape||shapeVerts.empty())return; auto[x0,y0]=shapeVerts.back();const int sg=bezierDetailVal; for(int i=1;i<=sg;i++){float t=i/(float)sg,u=1-t; float bx=u*u*u*x0+3*u*u*t*cx1+3*u*t*t*cx2+t*t*t*x, by=u*u*u*y0+3*u*u*t*cy1+3*u*t*t*cy2+t*t*t*y; shapeVerts.push_back({bx,by}); shapeVerts3D.push_back({bx,by,0.0f});} } void PApplet::quadraticVertex(float cx,float cy,float x,float y){ if(!inShape||shapeVerts.empty())return; auto[x0,y0]=shapeVerts.back();const int sg=bezierDetailVal; for(int i=1;i<=sg;i++){float t=i/(float)sg,u=1-t;float qx=u*u*x0+2*u*t*cx+t*t*x,qy=u*u*y0+2*u*t*cy+t*t*y;shapeVerts.push_back({qx,qy});shapeVerts3D.push_back({qx,qy,0.0f});} } void PApplet::curveVertex(float x,float y){if(inShape){shapeVerts.push_back({x,y});shapeVerts3D.push_back({x,y,0.0f});}} void PApplet::bezier(float x1,float y1,float cx1,float cy1,float cx2,float cy2,float x2,float y2){ if(!doStroke)return;applyStroke();glLineWidth(strokeW);glBegin(GL_LINE_STRIP); for(int i=0;i<=bezierDetailVal;i++){float t=i/(float)bezierDetailVal,u=1-t; glVertex2f(u*u*u*x1+3*u*u*t*cx1+3*u*t*t*cx2+t*t*t*x2,u*u*u*y1+3*u*u*t*cy1+3*u*t*t*cy2+t*t*t*y2);} glEnd(); } void PApplet::curve(float x0,float y0,float x1,float y1,float x2,float y2,float x3,float y3){ if(!doStroke)return;applyStroke();glLineWidth(strokeW);glBegin(GL_LINE_STRIP); float s=curveTightnessVal; for(int i=0;i<=curveDetailVal;i++){ float t=i/(float)curveDetailVal,t2=t*t,t3=t2*t; float b0=(-s*t3+2*s*t2-s*t)/2.f,b1=((2-s)*t3+(s-3)*t2+1)/2.f; float b2=((s-2)*t3+(3-2*s)*t2+s*t)/2.f,b3=(s*t3-s*t2)/2.f; glVertex2f(b0*x0+b1*x1+b2*x2+b3*x3,b0*y0+b1*y1+b2*y2+b3*y3); } glEnd(); } float PApplet::bezierPoint(float a,float b,float c,float d,float t){float u=1-t;return u*u*u*a+3*u*u*t*b+3*u*t*t*c+t*t*t*d;} float PApplet::bezierTangent(float a,float b,float c,float d,float t){float u=1-t;return 3*u*u*(b-a)+6*u*t*(c-b)+3*t*t*(d-c);} float PApplet::curvePoint(float a,float b,float c,float d,float t){float t2=t*t,t3=t2*t,s=curveTightnessVal;return 0.5f*((-s*t3+2*s*t2-s*t)*a+((2-s)*t3+(s-3)*t2+1)*b+((s-2)*t3+(3-2*s)*t2+s*t)*c+(s*t3-s*t2)*d);} float PApplet::curveTangent(float a,float b,float c,float d,float t){float t2=t*t,s=curveTightnessVal;return 0.5f*((-3*s*t2+4*s*t-s)*a+(3*(2-s)*t2+2*(s-3)*t)*b+(3*(s-2)*t2+2*(3-2*s)*t+s)*c+(3*s*t2-2*s*t)*d);} void PApplet::curveDetail(int d) {curveDetailVal=d;} void PApplet::curveTightness(float t){curveTightnessVal=t;} void PApplet::bezierDetail(int d) {bezierDetailVal=d;} // ============================================================================= // MATRIX // ============================================================================= void PApplet::resetMatrix(){glLoadIdentity();} void PApplet::applyMatrix(float n00,float n01,float n02,float n03,float n10,float n11,float n12,float n13,float n20,float n21,float n22,float n23,float n30,float n31,float n32,float n33){ float m[]={n00,n10,n20,n30,n01,n11,n21,n31,n02,n12,n22,n32,n03,n13,n23,n33}; glMultMatrixf(m); } void PApplet::translate(float x,float y) {glTranslatef(x,y,0);} void PApplet::translate(float x,float y,float z){glTranslatef(x,y,z);} void PApplet::scale(float s) {glScalef(s,s,1);} void PApplet::scale(float sx,float sy) {glScalef(sx,sy,1);} void PApplet::rotate(float a) {glRotatef(a*180.0f/PI,0,0,1);} void PApplet::shearX(float a) {float m[]={1,0,0,0,std::tan(a),1,0,0,0,0,1,0,0,0,0,1};glMultMatrixf(m);} void PApplet::shearY(float a) {float m[]={1,std::tan(a),0,0,0,1,0,0,0,0,1,0,0,0,0,1};glMultMatrixf(m);} void PApplet::printMatrix(){float m[16];glGetFloatv(GL_MODELVIEW_MATRIX,m);for(int i=0;i<4;i++){for(int j=0;j<4;j++)std::cout<0)fbW=fw;if(fh>0)fbH=fh;} glViewport(0, 0, fbW, fbH); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glScalef(1,-1,1); _gluPerspective(60.0, (double)logicalW / logicalH, near_, far_); applyStandardModelview(); } void PApplet::camera(){ applyDefaultCamera(); } void PApplet::camera(float ex,float ey,float ez,float cx,float cy,float cz,float ux,float uy,float uz){ float eyeZ = ((float)logicalH/2.0f) / std::tan(PI*60.0f/360.0f); float near_ = eyeZ/10.0f, far_ = eyeZ*10.0f; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glScalef(1,-1,1); _gluPerspective(60.0,(double)logicalW/logicalH,near_,far_); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); _gluLookAt(ex,ey,ez,cx,cy,cz,ux,uy,uz); glFrontFace(GL_CW); glDisable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); } void PApplet::beginCamera(){glMatrixMode(GL_MODELVIEW);glPushMatrix();} void PApplet::endCamera() {glPopMatrix();} void PApplet::perspective(){ applyDefaultCamera(); } void PApplet::perspective(float fov, float aspect, float zNear, float zFar) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); glScalef(1,-1,1); _gluPerspective(degrees(fov), aspect, zNear, zFar); applyStandardModelview(); } // Helper shared by ortho() and perspective() -- sets up the standard // Processing modelview camera (eye at eyeZ looking at canvas centre, // Y-down screen coordinates) and enables depth test. void PApplet::applyStandardModelview() { float eyeZ = ((float)logicalH / 2.0f) / std::tan(PI * 60.0f / 360.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); _gluLookAt(logicalW/2.0, logicalH/2.0, eyeZ, logicalW/2.0, logicalH/2.0, 0, 0, 1, 0); glFrontFace(GL_CW); glDisable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); } void PApplet::ortho() { // Default ortho: 1:1 pixel mapping, origin at top-left, Y increases downward. // glOrtho(l, r, bottom, top, near, far): // bottom = winHeight (screen bottom = large Y in Processing) // top = 0 (screen top = Y=0 in Processing) // gluLookAt is NOT used here -- we want raw screen-space coordinates, // so we use a simple identity modelview with Y-flipped glOrtho. glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, logicalW, logicalH, 0, -logicalH, logicalH); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glFrontFace(GL_CW); glDisable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); } void PApplet::ortho(float l, float r, float b, float t, float n, float f) { // Processing Java ortho() uses the SAME standard modelview as perspective() // (eye at eyeZ above canvas center, looking at canvas center). // Only the projection changes: orthographic instead of frustum. // The Y-flip (glScalef 1,-1,1) is applied in projection space so that // Processing's Y-down screen coordinate convention is preserved. // // With the standard camera, world point (w/2, h/2, 0) is at screen center. // ortho(-w/2, w/2, -h/2, h/2) covers that range around the camera target. // So translate(w/2, h/2) correctly lands at the center of the screen. glMatrixMode(GL_PROJECTION); glLoadIdentity(); glScalef(1,-1,1); // Y-flip before ortho (Java order) glOrtho(l, r, b, t, n, f); applyStandardModelview(); } void PApplet::frustum(float l, float r, float b, float t, float n, float f) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(l, r, b, t, n, f); applyStandardModelview(); } void PApplet::printCamera(){float m[16];glGetFloatv(GL_MODELVIEW_MATRIX,m);std::cout<<"Camera matrix:\n";for(int i=0;i<4;i++){for(int j=0;j<4;j++)std::cout<colorMaxH : v/255.f; } // respects colorMode max // Apply all common light state and reset the index counter. // Call this at the start of a draw() that uses lights. void PApplet::lights() { // Matches Processing Java lights() exactly: // ambientLight(128, 128, 128) // directionalLight(128, 128, 128, 0, 0, -1) // Result: shadow faces = 50% brightness, front face = 100%, sides interpolated. glEnable(GL_LIGHTING); glEnable(GL_NORMALIZE); glEnable(GL_COLOR_MATERIAL); // GL_COLOR_MATERIAL drives AMBIENT_AND_DIFFUSE from fill color glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); lightsEnabled = true; lightIndex = 0; // Zero out global ambient -- we set it explicitly via LIGHT0 ambient below GLfloat noGlobalAmb[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, noGlobalAmb); glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE); glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_FALSE); GLfloat zero[] = { 0.0f, 0.0f, 0.0f, 1.0f }; // LIGHT0: ambient component = 128/255 (matches Java ambientLight(128,128,128)) // This contributes equally to all faces regardless of normal. GLfloat amb[] = { 0.502f, 0.502f, 0.502f, 1.0f }; // 128/255 // LIGHT0: diffuse component = 128/255 (matches Java directionalLight(128,128,128,...)) GLfloat dif[] = { 0.502f, 0.502f, 0.502f, 1.0f }; // 128/255 // Direction (0,0,-1) in Java = light travels toward -Z = toward-light vector (0,0,1) // In eye space with default camera looking down -Z, (0,0,1) points at camera = front-lit GLfloat pos[] = { 0.0f, 0.0f, 1.0f, 0.0f }; // w=0 = directional glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_AMBIENT, amb); glLightfv(GL_LIGHT0, GL_DIFFUSE, dif); glLightfv(GL_LIGHT0, GL_SPECULAR, zero); // Set in eye space so light direction is fixed relative to camera glPushMatrix(); glLoadIdentity(); glLightfv(GL_LIGHT0, GL_POSITION, pos); glPopMatrix(); lightIndex = 1; { GLint curFbo = 0; glGetIntegerv(GL_FRAMEBUFFER_BINDING, &curFbo); double mv[16]; glGetDoublev(GL_MODELVIEW_MATRIX, mv); GLboolean depthTestOn = glIsEnabled(GL_DEPTH_TEST); GLboolean lightingOn = glIsEnabled(GL_LIGHTING); GLboolean colorMatOn = glIsEnabled(GL_COLOR_MATERIAL); GLfloat curFillColor[4]; glGetFloatv(GL_CURRENT_COLOR, curFillColor); PDEBUG("PApplet::lights() TRACE: curFbo=%d depthTest=%d lighting=%d colorMaterial=%d\n", curFbo, depthTestOn, lightingOn, colorMatOn); PDEBUG(" mv[0]=%.3f mv[5]=%.3f mv[10]=%.3f mv[12]=%.3f mv[13]=%.3f mv[14]=%.3f\n", mv[0], mv[5], mv[10], mv[12], mv[13], mv[14]); double proj[16]; glGetDoublev(GL_PROJECTION_MATRIX, proj); PDEBUG(" proj[0]=%.3f proj[5]=%.3f proj[10]=%.3f proj[11]=%.3f proj[14]=%.3f\n", proj[0], proj[5], proj[10], proj[11], proj[14]); PDEBUG(" currentColor (glColor state at this moment): r=%.3f g=%.3f b=%.3f a=%.3f\n", curFillColor[0], curFillColor[1], curFillColor[2], curFillColor[3]); } } void PApplet::noLights() { glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); lightsEnabled = false; lightIndex = 0; } // Ambient light: emits equally in all directions, no diffuse. void PApplet::ambientLight(float r, float g, float b) { ambientLight(r,g,b,0,0,0); } void PApplet::ambientLight(float r, float g, float b, float x, float y, float z) { if (lightIndex >= 8) return; GLenum lt = GL_LIGHT0 + lightIndex++; GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; GLfloat pos[] = { x, y, z, 1.0f }; GLfloat zero[]= { 0.0f, 0.0f, 0.0f, 1.0f }; glEnable(GL_LIGHTING); glEnable(GL_NORMALIZE); glEnable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE); glEnable(lt); glLightfv(lt, GL_AMBIENT, col); glLightfv(lt, GL_DIFFUSE, zero); glLightfv(lt, GL_SPECULAR, zero); // Position is transformed by the current modelview (world space) glLightfv(lt, GL_POSITION, pos); } // Directional light: parallel rays from an infinite distance (w=0). // 'nx,ny,nz' is the direction the light TRAVELS (toward the scene). // OpenGL position with w=0 points TOWARD the light source, so we negate. void PApplet::directionalLight(float r, float g, float b, float nx, float ny, float nz) { if (lightIndex >= 8) return; GLenum lt = GL_LIGHT0 + lightIndex++; GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; // Negate direction and flip Y to compensate for glScalef(1,-1,1) in modelview GLfloat pos[] = { -nx, ny, -nz, 0.0f }; GLfloat zero[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glEnable(GL_LIGHTING); glEnable(GL_NORMALIZE); glDisable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); glEnable(GL_COLOR_MATERIAL); // No global ambient for directionalLight alone (matches Java Processing) GLfloat gAmb[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gAmb); lightsEnabled = true; GLfloat spec[] = { pendingSpecR, pendingSpecG, pendingSpecB, 1.0f }; glEnable(lt); glLightfv(lt, GL_AMBIENT, zero); glLightfv(lt, GL_DIFFUSE, col); glLightfv(lt, GL_SPECULAR, spec); // Set in pure eye space (no world transforms) like Java Processing. // toward-light in eye space: negate direction, flip Y for our Y-down convention. glPushMatrix(); glLoadIdentity(); // toward-light = negate direction. Y negated because Processing Y-down maps to eye Y-up // (glScalef(1,-1,1) in projection flips screen Y, so world +Y = eye -Y) GLfloat posE[] = { -nx, -ny, -nz, 0.0f }; glLightfv(lt, GL_POSITION, posE); glPopMatrix(); } // Point light: emits in all directions from a world-space position. void PApplet::pointLight(float r, float g, float b, float x, float y, float z) { if (lightIndex >= 8) return; GLenum lt = GL_LIGHT0 + lightIndex++; GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; GLfloat pos[] = { x, y, z, 1.0f }; // w=1 = positional GLfloat zero[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glEnable(GL_LIGHTING); glEnable(GL_NORMALIZE); glEnable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); glEnable(lt); glLightfv(lt, GL_AMBIENT, zero); glLightfv(lt, GL_DIFFUSE, col); glLightfv(lt, GL_SPECULAR, zero); glLightf(lt, GL_CONSTANT_ATTENUATION, 1.0f); glLightf(lt, GL_LINEAR_ATTENUATION, 0.0f); glLightf(lt, GL_QUADRATIC_ATTENUATION, 0.0f); // Position transformed by current modelview (world space) glLightfv(lt, GL_POSITION, pos); } // Spot light: cone of light from a position toward a direction. // angle = half-angle of the cone in radians (OpenGL takes degrees) // conc = concentration exponent (higher = tighter beam) void PApplet::spotLight(float r, float g, float b, float x, float y, float z, float nx, float ny, float nz, float angle, float conc) { // Java Processing sets spotlight pos/dir through the CURRENT modelview // (which includes LookAt * Scale(1,-1,1)) -- same as world space draw. // We flip Y to compensate for our glScalef(1,-1,1) baked into modelview. if (lightIndex >= 8) return; GLenum lt = GL_LIGHT0 + lightIndex++; GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; GLfloat zero[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glEnable(GL_LIGHTING); glEnable(GL_NORMALIZE); glDisable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); glEnable(GL_COLOR_MATERIAL); lightsEnabled = true; glEnable(lt); glLightfv(lt, GL_AMBIENT, zero); glLightfv(lt, GL_DIFFUSE, col); glLightfv(lt, GL_SPECULAR, zero); // Store concentration and cutoff cosine for the shader // (GL_SPOT_EXPONENT caps at 128, but Java uses values like 600) int li = lightIndex - 1; lightConcentration[li] = conc; lightCutoffCos[li] = std::cos(angle); // precompute cos(cutoff) // Still set GL_SPOT_CUTOFF so fixed-function fallback works float cutDeg = std::min(angle * 180.0f / PI, 90.0f); glLightf(lt, GL_SPOT_CUTOFF, cutDeg); glLightf(lt, GL_SPOT_EXPONENT, std::min(conc, 128.0f)); // capped for GL glLightf(lt, GL_CONSTANT_ATTENUATION, 1.0f); glLightf(lt, GL_LINEAR_ATTENUATION, 0.0f); glLightf(lt, GL_QUADRATIC_ATTENUATION, 0.0f); // Position: NO Y-flip (modelview Scale already handles it correctly) // Direction: Y-flip ny to compensate for Scale(1,-1,1) in modelview GLfloat posW[] = { x, y, z, 1.0f }; GLfloat dirW[] = { nx, -ny, nz }; glLightfv(lt, GL_POSITION, posW); glLightfv(lt, GL_SPOT_DIRECTION, dirW); } // Override attenuation on all active lights (call after the light functions) void PApplet::lightFalloff(float c, float l, float q) { for (int i = 0; i < lightIndex; i++) { GLenum lt = GL_LIGHT0 + i; glLightf(lt, GL_CONSTANT_ATTENUATION, c); glLightf(lt, GL_LINEAR_ATTENUATION, l); glLightf(lt, GL_QUADRATIC_ATTENUATION, q); } } // Set specular colour on all active lights void PApplet::lightSpecular(float r, float g, float b) { // Store as pending so subsequent light calls pick it up pendingSpecR = lc(r); pendingSpecG = lc(g); pendingSpecB = lc(b); // Also apply to any already-created lights for (int i = 0; i < lightIndex; i++) { GLenum lt = GL_LIGHT0 + i; GLfloat col[] = { pendingSpecR, pendingSpecG, pendingSpecB, 1.0f }; glLightfv(lt, GL_SPECULAR, col); } } void PApplet::normal(float nx, float ny, float nz) { glNormal3f(nx, ny, nz); } // ============================================================================= // MATERIAL // ============================================================================= void PApplet::ambient(float r, float g, float b) { GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, col); } void PApplet::ambient(color c) { unsigned int v = c.value; ambient((v >> 16 & 0xFF) / 255.f, (v >> 8 & 0xFF) / 255.f, (v & 0xFF) / 255.f); } void PApplet::emissive(float r, float g, float b) { GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, col); } void PApplet::emissive(color c) { unsigned int v = c.value; emissive((v >> 16 & 0xFF) / 255.f, (v >> 8 & 0xFF) / 255.f, (v & 0xFF) / 255.f); } void PApplet::specular(float r, float g, float b) { GLfloat col[] = { lc(r), lc(g), lc(b), 1.0f }; glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, col); } void PApplet::specular(color c) { unsigned int v = c.value; specular((v >> 16 & 0xFF) / 255.f, (v >> 8 & 0xFF) / 255.f, (v & 0xFF) / 255.f); } void PApplet::shininess(float s) { glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, s); } // ============================================================================= // TEXT // ============================================================================= // -- Embedded 6x8 bitmap font fallback (ASCII 32-126) ------------------------- static const unsigned char g_font6x8[][6] = { {0x00,0x00,0x00,0x00,0x00,0x00},{0x04,0x04,0x04,0x04,0x00,0x04},{0x0A,0x0A,0x00,0x00,0x00,0x00},{0x0A,0x1F,0x0A,0x1F,0x0A,0x00},{0x0E,0x15,0x1C,0x07,0x15,0x0E},{0x19,0x1A,0x02,0x04,0x0B,0x13},{0x08,0x14,0x08,0x15,0x12,0x0D},{0x04,0x04,0x00,0x00,0x00,0x00}, {0x02,0x04,0x04,0x04,0x04,0x02},{0x08,0x04,0x04,0x04,0x04,0x08},{0x00,0x0A,0x04,0x1F,0x04,0x0A},{0x00,0x04,0x04,0x1F,0x04,0x04},{0x00,0x00,0x00,0x00,0x04,0x08},{0x00,0x00,0x00,0x1F,0x00,0x00},{0x00,0x00,0x00,0x00,0x00,0x04},{0x01,0x02,0x02,0x04,0x08,0x10}, {0x0E,0x11,0x13,0x15,0x19,0x0E},{0x04,0x0C,0x04,0x04,0x04,0x0E},{0x0E,0x11,0x02,0x04,0x08,0x1F},{0x1F,0x02,0x06,0x01,0x11,0x0E},{0x02,0x06,0x0A,0x1F,0x02,0x02},{0x1F,0x10,0x1E,0x01,0x11,0x0E},{0x06,0x08,0x1E,0x11,0x11,0x0E},{0x1F,0x01,0x02,0x04,0x04,0x04}, {0x0E,0x11,0x0E,0x11,0x11,0x0E},{0x0E,0x11,0x0F,0x01,0x02,0x0C},{0x00,0x04,0x00,0x00,0x04,0x00},{0x00,0x04,0x00,0x00,0x04,0x08},{0x02,0x04,0x08,0x08,0x04,0x02},{0x00,0x1F,0x00,0x1F,0x00,0x00},{0x08,0x04,0x02,0x02,0x04,0x08},{0x0E,0x11,0x02,0x04,0x00,0x04}, {0x0E,0x11,0x17,0x15,0x17,0x0E},{0x04,0x0A,0x11,0x1F,0x11,0x11},{0x1E,0x11,0x1E,0x11,0x11,0x1E},{0x0E,0x11,0x10,0x10,0x11,0x0E},{0x1C,0x12,0x11,0x11,0x12,0x1C},{0x1F,0x10,0x1E,0x10,0x10,0x1F},{0x1F,0x10,0x1E,0x10,0x10,0x10},{0x0E,0x11,0x10,0x17,0x11,0x0F}, {0x11,0x11,0x1F,0x11,0x11,0x11},{0x0E,0x04,0x04,0x04,0x04,0x0E},{0x07,0x02,0x02,0x02,0x12,0x0C},{0x11,0x12,0x1C,0x12,0x11,0x11},{0x10,0x10,0x10,0x10,0x10,0x1F},{0x11,0x1B,0x15,0x11,0x11,0x11},{0x11,0x19,0x15,0x13,0x11,0x11},{0x0E,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10},{0x0E,0x11,0x11,0x15,0x12,0x0D},{0x1E,0x11,0x11,0x1E,0x11,0x11},{0x0F,0x10,0x0E,0x01,0x11,0x0E},{0x1F,0x04,0x04,0x04,0x04,0x04},{0x11,0x11,0x11,0x11,0x11,0x0E},{0x11,0x11,0x11,0x0A,0x0A,0x04},{0x11,0x11,0x15,0x15,0x1B,0x11}, {0x11,0x0A,0x04,0x04,0x0A,0x11},{0x11,0x0A,0x04,0x04,0x04,0x04},{0x1F,0x02,0x04,0x08,0x10,0x1F},{0x0E,0x08,0x08,0x08,0x08,0x0E},{0x10,0x08,0x08,0x04,0x02,0x01},{0x0E,0x02,0x02,0x02,0x02,0x0E},{0x04,0x0A,0x11,0x00,0x00,0x00},{0x00,0x00,0x00,0x00,0x00,0x1F}, {0x08,0x04,0x00,0x00,0x00,0x00},{0x00,0x0E,0x01,0x0F,0x11,0x0F},{0x10,0x10,0x1E,0x11,0x11,0x1E},{0x00,0x0E,0x10,0x10,0x10,0x0E},{0x01,0x01,0x0F,0x11,0x11,0x0F},{0x00,0x0E,0x11,0x1F,0x10,0x0E},{0x06,0x08,0x1E,0x08,0x08,0x08},{0x00,0x0F,0x11,0x0F,0x01,0x0E}, {0x10,0x10,0x1E,0x11,0x11,0x11},{0x04,0x00,0x04,0x04,0x04,0x0E},{0x02,0x00,0x02,0x02,0x12,0x0C},{0x10,0x12,0x14,0x1C,0x12,0x11},{0x0C,0x04,0x04,0x04,0x04,0x0E},{0x00,0x1B,0x15,0x15,0x11,0x11},{0x00,0x1E,0x11,0x11,0x11,0x11},{0x00,0x0E,0x11,0x11,0x11,0x0E}, {0x00,0x1E,0x11,0x1E,0x10,0x10},{0x00,0x0F,0x11,0x0F,0x01,0x01},{0x00,0x16,0x19,0x10,0x10,0x10},{0x00,0x0E,0x10,0x0E,0x01,0x1E},{0x08,0x1F,0x08,0x08,0x08,0x07},{0x00,0x11,0x11,0x11,0x13,0x0D},{0x00,0x11,0x11,0x0A,0x0A,0x04},{0x00,0x11,0x15,0x15,0x1B,0x11}, {0x00,0x11,0x0A,0x04,0x0A,0x11},{0x00,0x11,0x0A,0x04,0x08,0x10},{0x00,0x1F,0x02,0x04,0x08,0x1F},{0x06,0x04,0x0C,0x04,0x04,0x06},{0x04,0x04,0x04,0x04,0x04,0x04},{0x0C,0x04,0x06,0x04,0x04,0x0C},{0x08,0x15,0x02,0x00,0x00,0x00}, }; // -- stb_truetype font state --------------------------------------------------- #if PROCESSING_HAS_STB_TRUETYPE bool PApplet::loadTTFFile(const std::string& path) { std::ifstream f(path, std::ios::binary); if (!f) return false; // Load into local vector first, then move — ensures data pointer is stable // before stbtt_InitFont stores a reference into it. std::vector tmp( (std::istreambuf_iterator(f)), std::istreambuf_iterator()); if (tmp.empty()) return false; g_ttf.data = std::move(tmp); g_ttf.data.shrink_to_fit(); // pin allocation before passing pointer to stbtt return stbtt_InitFont(&g_ttf.info, g_ttf.data.data(), 0) != 0; } void PApplet::bakeAtlas(float pixelSize) { if (!g_ttf.loaded) return; // Cache key: rounded to nearest 0.5px to avoid float noise int cacheKey = (int)std::round(pixelSize * 2.0f); // Check cache — if already baked this size, just activate it auto it = g_ttf.atlasCache.find(cacheKey); if (it != g_ttf.atlasCache.end()) { g_ttf.current = &it->second; g_ttf.texID = g_ttf.current->texID; g_ttf.chars = g_ttf.current->chars; g_ttf.bakeSize = pixelSize; return; } // Not cached — bake a new atlas for this size TTFAtlas atlas; g_ttf.atlasW = 1024; g_ttf.atlasH = 1024; std::vector bitmap(g_ttf.atlasW * g_ttf.atlasH); int ret = stbtt_BakeFontBitmap( g_ttf.data.data(), 0, pixelSize, bitmap.data(), g_ttf.atlasW, g_ttf.atlasH, 32, 96, atlas.chars); if (ret == 0) { g_ttf.loaded = false; return; } glGenTextures(1, &atlas.texID); glBindTexture(GL_TEXTURE_2D, atlas.texID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); std::vector rgba(g_ttf.atlasW * g_ttf.atlasH * 4); for (int i = 0; i < g_ttf.atlasW * g_ttf.atlasH; i++) { rgba[i*4+0] = rgba[i*4+1] = rgba[i*4+2] = 255; rgba[i*4+3] = bitmap[i]; } glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, g_ttf.atlasW, g_ttf.atlasH, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); glBindTexture(GL_TEXTURE_2D, 0); // Store in cache g_ttf.atlasCache[cacheKey] = std::move(atlas); g_ttf.current = &g_ttf.atlasCache[cacheKey]; g_ttf.texID = g_ttf.current->texID; g_ttf.chars = g_ttf.current->chars; g_ttf.bakeSize = pixelSize; } #endif // PROCESSING_HAS_STB_TRUETYPE // -- Shared text state --------------------------------------------------------- // -- Bitmap font rendering (fallback) ----------------------------------------- static const int BF_GW = 6; static const int BF_GH = 8; void PApplet::drawBitmapStr(float x, float y, const std::string& s, int scale) { glColor4f(fillR, fillG, fillB, fillA); glDisable(GL_TEXTURE_2D); float cx = x; for (char ch : s) { if (ch < 32 || ch > 126) { cx += (BF_GW+1)*scale; continue; } const unsigned char* bm = g_font6x8[(unsigned char)ch - 32]; for (int row = 0; row < BF_GH; row++) { unsigned char bits = bm[row]; for (int col = 0; col < BF_GW; col++) { if (bits & (1 << (BF_GW - 1 - col))) { glBegin(GL_QUADS); float px = cx + col*scale, py = y + row*scale; glVertex2f(px, py); glVertex2f(px+scale, py); glVertex2f(px+scale, py+scale); glVertex2f(px, py+scale); glEnd(); } } } cx += (BF_GW+1)*scale; } } float PApplet::bitmapStrWidth(const std::string& s, int scale) { return s.size() * (BF_GW+1) * scale; } // -- TTF rendering ------------------------------------------------------------- #if PROCESSING_HAS_STB_TRUETYPE float PApplet::ttfStrWidth(const std::string& s) { float x = 0; for (char ch : s) { if (ch < 32 || ch > 127) continue; int advance, lsb; stbtt_GetCodepointHMetrics(&g_ttf.info, ch, &advance, &lsb); float sc = stbtt_ScaleForMappingEmToPixels(&g_ttf.info, g_textSize); x += advance * sc; } return x; } void PApplet::drawTTFStr(float x, float y, const std::string& s) { if (!g_ttf.loaded) return; bakeAtlas(g_textSize); // BUG FIX: same class of issue found in drawPGraphicsRect() -- if // lights() was called earlier this frame, GL_LIGHTING stays globally // enabled, and text glyph quads (no normal ever set) get lit/ // darkened unexpectedly. Text should never be affected by whatever // 3D lighting state happens to be active. GLboolean wasLighting = glIsEnabled(GL_LIGHTING); GLboolean wasDepthTest = glIsEnabled(GL_DEPTH_TEST); if (wasLighting) glDisable(GL_LIGHTING); if (wasDepthTest) glDisable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, g_ttf.texID); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Use fill colour modulated by font alpha glColor4f(fillR, fillG, fillB, fillA); glBegin(GL_QUADS); float cx = x; for (char ch : s) { if (ch < 32 || ch >= 128) continue; stbtt_aligned_quad q; stbtt_GetBakedQuad(g_ttf.chars, g_ttf.atlasW, g_ttf.atlasH, ch - 32, &cx, &y, &q, 1); glTexCoord2f(q.s0, q.t0); glVertex2f(q.x0, q.y0); glTexCoord2f(q.s1, q.t0); glVertex2f(q.x1, q.y0); glTexCoord2f(q.s1, q.t1); glVertex2f(q.x1, q.y1); glTexCoord2f(q.s0, q.t1); glVertex2f(q.x0, q.y1); } glEnd(); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); if (wasLighting) glEnable(GL_LIGHTING); if (wasDepthTest) glEnable(GL_DEPTH_TEST); } #endif // -- Main renderText entry point ----------------------------------------------- float PApplet::getLineWidth(const std::string& line) { #if PROCESSING_HAS_STB_TRUETYPE if (g_ttf.loaded) return ttfStrWidth(line); #endif int sc = std::max(1,(int)(g_textSize/8.0f)); return bitmapStrWidth(line, sc); } void PApplet::renderText(const std::string& msg, float x, float y) { if (msg.empty()) return; float leading = (g_textLeading > 0) ? g_textLeading : g_textSize * 1.25f; // Split on \n std::vector<:string> ls; std::string cur; for (char c : msg) { if(c=='\n'){ls.push_back(cur);cur.clear();}else cur+=c; } ls.push_back(cur); for (int li = 0; li < (int)ls.size(); li++) { float lw = getLineWidth(ls[li]); float dx = x; if (g_textAlignX == RIGHT_ALIGN) dx = x - lw; else if (g_textAlignX == CENTER_ALIGN) dx = x - lw * 0.5f; float dy = y + li * leading; #if PROCESSING_HAS_STB_TRUETYPE if (g_ttf.loaded) { drawTTFStr(dx, dy, ls[li]); continue; } #endif // Bitmap fallback int sc = std::max(1,(int)(g_textSize/8.0f)); // Shift so baseline sits at y (bitmap font: ascent = BF_GH-2 rows) float ascent = (BF_GH - 2) * sc; drawBitmapStr(dx, dy - ascent, ls[li], sc); } } void PApplet::text(const std::string& msg, float x, float y) { renderText(msg, x, y); } void PApplet::text(int v, float x, float y) { renderText(std::to_string(v), x, y); } void PApplet::text(float v, float x, float y) { std::ostringstream ss; ss << v; renderText(ss.str(), x, y); } // text(str, x, y, w, h) -- word-wrap into a bounding box, matching Processing Java. // Words are wrapped when they exceed width w. Lines are clipped at height h. // x,y is top-left corner of the box. Text starts at the baseline of the first line. void PApplet::text(const std::string& msg, float x, float y, float w, float h) { if (msg.empty() || w <= 0) return; float leading = (g_textLeading > 0) ? g_textLeading : g_textSize * 1.4f; float maxY = (h > 0) ? y + h : 1e9f; // Split message into tokens: words and explicit newlines std::vector<:string> tokens; { std::string cur; for (char c : msg) { if (c == '\n') { if (!cur.empty()) { tokens.push_back(cur); cur.clear(); } tokens.push_back("\n"); } else if (c == ' ') { if (!cur.empty()) { tokens.push_back(cur); cur.clear(); } } else { cur += c; } } if (!cur.empty()) tokens.push_back(cur); } // Layout: accumulate words into lines, wrap when line width exceeds w float cy = y; std::string line; auto flushLine = [&](){ if (!line.empty() && cy + g_textSize <= maxY) { renderText(line, x, cy); } line.clear(); cy += leading; }; for (auto& tok : tokens) { if (tok == "\n") { // Explicit newline: flush current line and advance if (cy + g_textSize <= maxY) renderText(line, x, cy); line.clear(); cy += leading; continue; } // Try adding this word to the current line std::string candidate = line.empty() ? tok : line + " " + tok; float cw = getLineWidth(candidate); if (!line.empty() && cw > w) { // Word doesn't fit: flush current line, start new line with this word if (cy + g_textSize <= maxY) renderText(line, x, cy); line.clear(); cy += leading; // If the word itself is wider than w, wrap it character by character if (getLineWidth(tok) > w) { std::string charLine; for (char c : tok) { std::string test2 = charLine + c; if (!charLine.empty() && getLineWidth(test2) > w) { if (cy + g_textSize <= maxY) renderText(charLine, x, cy); charLine.clear(); cy += leading; } charLine += c; } line = charLine; } else { line = tok; } } else if (line.empty() && cw > w) { // First word on line is too wide: wrap char by char std::string charLine; for (char c : tok) { std::string test2 = charLine + c; if (!charLine.empty() && getLineWidth(test2) > w) { if (cy + g_textSize <= maxY) renderText(charLine, x, cy); charLine.clear(); cy += leading; } charLine += c; } line = charLine; } else { line = candidate; } } // Flush remaining text if (!line.empty() && cy + g_textSize <= maxY) { renderText(line, x, cy); } } void PApplet::textSize(float size) { g_textSize = size; #if PROCESSING_HAS_STB_TRUETYPE if (g_ttf.loaded) bakeAtlas(size); #endif } void PApplet::textAlign(int alignX, int alignY) { // Map standard constants (LEFT=37, RIGHT=39, CENTER=3) to align constants auto mapAlign=[](int a)->int{ if(a==37||a==20) return LEFT_ALIGN; // LEFT or LEFT_ALIGN if(a==39||a==21) return RIGHT_ALIGN; // RIGHT or RIGHT_ALIGN if(a==3 ||a==25) return CENTER_ALIGN; // CENTER or CENTER_ALIGN if(a==0) return BASELINE; return a; }; g_textAlignX = mapAlign(alignX); if(alignY >= 0) g_textAlignY = mapAlign(alignY); } void PApplet::textLeading(float v) { g_textLeading = v; } void PApplet::textMode(int) {} float PApplet::textWidth(const std::string& s) { return getLineWidth(s); } float PApplet::textAscent() { #if PROCESSING_HAS_STB_TRUETYPE if (g_ttf.loaded) { float sc = stbtt_ScaleForMappingEmToPixels(&g_ttf.info, g_textSize); int asc; stbtt_GetFontVMetrics(&g_ttf.info, &asc, nullptr, nullptr); return asc * sc; } #endif int sc = std::max(1,(int)(g_textSize/8.0f)); return (BF_GH - 2) * sc; } float PApplet::textDescent() { #if PROCESSING_HAS_STB_TRUETYPE if (g_ttf.loaded) { float sc = stbtt_ScaleForMappingEmToPixels(&g_ttf.info, g_textSize); int desc; stbtt_GetFontVMetrics(&g_ttf.info, nullptr, &desc, nullptr); return std::fabs(desc * sc); } #endif int sc = std::max(1,(int)(g_textSize/8.0f)); return 2.0f * sc; } // ============================================================================= // IMAGE // ============================================================================= PImage* PApplet::createImage(int w,int h,int mode){ PImage* img = new PImage(w,h); if(mode==3/*ARGB*/) { std::fill(img->pixels.begin(),img->pixels.end(),0x00000000); } img->dirty=true; return img; } PImage* PApplet::loadImage(const std::string& path){ // Handle URLs: download with curl/wget and validate image magic bytes if (path.size()>7 && (path.substr(0,7)=="http://" || path.substr(0,8)=="https://")){ #ifdef _WIN32 std::string tmp=std::string(getenv("TEMP")?getenv("TEMP"):"C:\\Temp")+"\\pg_img_"; #else std::string tmp="/tmp/pg_img_"; #endif size_t sl=path.rfind('/'); std::string bn=(sl!=std::string::npos)?path.substr(sl+1):"img.png"; { size_t q=bn.find('?'); if(q!=std::string::npos) bn=bn.substr(0,q); } if(bn.empty()||bn.find('.')==std::string::npos) bn="img.png"; for(char& c:bn) if(c==':'||c=='*'||c=='<'||c=='>'||c=='|') c='_'; tmp+=bn; auto isValidImg=[&]()->bool{ FILE* f2=fopen(tmp.c_str(),"rb"); if(!f2) return false; fseek(f2,0,SEEK_END); long sz=ftell(f2); fseek(f2,0,SEEK_SET); unsigned char h[4]={}; fread(h,1,4,f2); fclose(f2); if(sz<100) return false; return (h[0]==0x89&&h[1]=='P')|| // PNG (h[0]==0xFF&&h[1]==0xD8)|| // JPEG (h[0]=='G'&&h[1]=='I')|| // GIF (h[0]=='B'&&h[1]=='M')|| // BMP (h[0]=='R'&&h[1]=='I'); // WEBP }; // Try curl #ifdef _WIN32 system(("curl -sL --max-time 15 -o \""+tmp+"\" \""+path+"\"").c_str()); #else system(("curl -sL --max-time 15 -o '"+tmp+"' '"+path+"'").c_str()); #endif if(!isValidImg()){ // Try wget as fallback #ifdef _WIN32 system(("wget -q -O \""+tmp+"\" \""+path+"\"").c_str()); #else system(("wget -q -O '"+tmp+"' '"+path+"'").c_str()); #endif } if(isValidImg()){ PImage* r2=loadImage(tmp); if(r2&&r2->width>0) return r2; } fprintf(stderr,"[loadImage] URL download failed: %s\n",path.c_str()); return nullptr; } // Search paths: current dir, data/, files/, and sketch subdirs // Check PROCESSING_SKETCH_PATH env var set by IDE std::string _sketchDir; if (const char* _sp = std::getenv("PROCESSING_SKETCH_PATH")) _sketchDir = std::string(_sp) + "/"; // Also get the directory of the running executable std::string _exeDir; { char _buf[4096] = {}; #ifdef _WIN32 GetModuleFileNameA(nullptr, _buf, sizeof(_buf)); std::string _ep(_buf); size_t _sl = _ep.find_last_of("\\\\"); #else ssize_t _len = readlink("/proc/self/exe", _buf, sizeof(_buf)-1); if (_len > 0) _buf[_len] = 0; std::string _ep(_buf); size_t _sl = _ep.find_last_of("/"); #endif if (_sl != std::string::npos) _exeDir = _ep.substr(0, _sl+1); } std::vector<:string> tries = { path, "data/" + path, "files/" + path, "../data/" + path, _sketchDir + path, _sketchDir + "data/" + path, _exeDir + path, _exeDir + "data/" + path, // in case running from a subdirectory }; #ifdef PROCESSING_HAS_STB_IMAGE for (auto& p : tries) { int w, h, ch; unsigned char* data = stbi_load(p.c_str(), &w, &h, &ch, 4); if (!data) continue; PImage* img = new PImage(w, h); for (int i = 0; i < w*h; i++) { unsigned char r=data[i*4+0], g=data[i*4+1], b=data[i*4+2], a=data[i*4+3]; img->pixels[i] = ((unsigned int)a<<24)|((unsigned int)r<<16)|((unsigned int)g<<8)|b; } stbi_image_free(data); img->dirty = true; return img; } #endif for (auto& p : tries) std::cerr << "[loadImage] not found: " << p << "\n"; std::cerr << "[loadImage] returning empty image -- check the path above\n"; return new PImage(); // return empty (not null) so -> calls are safe } PGraphics* PApplet::createGraphics(int w,int h){return new PGraphics(w,h);} PGraphics* PApplet::createGraphics(int w,int h,int renderer){ // The renderer argument genuinely matters: a P3D buffer needs // beginDraw() to set up a real perspective projection with depth // testing enabled, instead of the flat 2D ortho projection (depth // range -1..1) used for plain 2D buffers -- without this, any 3D // content drawn into the buffer (box(), sphere(), translate(...,z)) // gets clipped away entirely by the paper-thin depth range, and // lighting/depth-testing never activates at all. See PGraphics:: // beginDraw()'s is3D branch for the actual projection setup. return new PGraphics(w, h, renderer == P3D); } // ── PImage::uploadTexture ──────────────────────────────────────────────────── void PImage::uploadTexture() { if (width <= 0 || height <= 0 || pixels.empty()) return; if ((int)pixels.size() < width * height) return; std::vector rgba((size_t)width * height * 4); for (int i = 0; i < width*height; i++) { unsigned int p = pixels[i]; rgba[i*4+0] = (p>>16)&0xFF; rgba[i*4+1] = (p>>8) &0xFF; rgba[i*4+2] = p &0xFF; rgba[i*4+3] = (p>>24)&0xFF; } if (texID == 0) glGenTextures(1, &texID); glBindTexture(GL_TEXTURE_2D, texID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); glBindTexture(GL_TEXTURE_2D, 0); dirty = false; } void PApplet::drawImageRect(PImage& img,float x,float y,float w,float h){ if(img.width==0||img.height==0) return; if(img.dirty) img.uploadTexture(); if(img.texID==0) return; glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D,img.texID); glColor4f(doTint?tintR:1.f,doTint?tintG:1.f,doTint?tintB:1.f,doTint?tintA:1.f); glBegin(GL_QUADS); glTexCoord2f(0,0);glVertex2f(x,y); glTexCoord2f(1,0);glVertex2f(x+w,y); glTexCoord2f(1,1);glVertex2f(x+w,y+h); glTexCoord2f(0,1);glVertex2f(x,y+h); glEnd(); glBindTexture(GL_TEXTURE_2D,0); glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); // Restore white so non-textured drawing after image() looks correct glColor4f(1.f,1.f,1.f,1.f); } // ── image() canonical implementation ───────────────────────────────────────── // Single entry point: everything goes through drawImage_impl. // Takes a raw PImage pointer so no reference/ABI issues across TUs. void PApplet::drawImage_impl(PImage* img, float x, float y, float w, float h) { if (!img || img->width == 0 || img->height == 0) return; // Apply imageMode to x,y,w,h float dx = x, dy = y, dw = w, dh = h; if (currentImageMode == CENTER) { dx -= dw*0.5f; dy -= dh*0.5f; } else if (currentImageMode == CORNERS) { dw = w - x; dh = h - y; dx = x; dy = y; } drawImageRect(*img, dx, dy, dw, dh); } // ── Public image() entry points ───────────────────────────────────────────── void PApplet::drawPGraphicsRect(PGraphics& pg, float x, float y, float w, float h){ PDEBUG("drawPGraphicsRect ENTRY: requested x=%.1f y=%.1f w=%.1f h=%.1f | pg.width=%d pg.height=%d | pg.texID=%u\n", x, y, w, h, pg.width, pg.height, pg.texID); if(pg.width==0||pg.height==0) { PDEBUG(" -> early return: width or height is 0\n"); return; } if(pg.texID==0) { PDEBUG(" -> early return: texID is 0\n"); return; } // BUG FIX: if lights() was called earlier this frame (e.g. drawing a // lit 3D scene on the main canvas before displaying this buffer), // GL_LIGHTING/GL_COLOR_MATERIAL/GL_DEPTH_TEST are all still globally // enabled at this point -- nothing here ever disabled them. Drawing // this textured quad with lighting still active means OpenGL tries // to light it like a 3D surface (with no normal ever set for it, // since this is meant to be a plain 2D blit), which can darken or // otherwise corrupt the displayed colors even though the texture's // actual stored content is correct. A 2D image blit should never be // affected by whatever 3D lighting state happens to be active. GLboolean wasLighting = glIsEnabled(GL_LIGHTING); GLboolean wasDepthTest = glIsEnabled(GL_DEPTH_TEST); if (wasLighting) glDisable(GL_LIGHTING); if (wasDepthTest) glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D,pg.texID); glColor4f(1,1,1,1); glBegin(GL_QUADS); // Y-up FBO with Processing Y-down remap: Processing y=0 -> v=1 (top of texture) // Display with V-flip: v=1 at screen top = Processing y=0 glTexCoord2f(0,0);glVertex2f(x,y); glTexCoord2f(1,0);glVertex2f(x+w,y); glTexCoord2f(1,1);glVertex2f(x+w,y+h); glTexCoord2f(0,1);glVertex2f(x,y+h); glEnd(); glBindTexture(GL_TEXTURE_2D,0); glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); glColor4f(1,1,1,1); if (wasLighting) glEnable(GL_LIGHTING); if (wasDepthTest) glEnable(GL_DEPTH_TEST); } void PApplet::image(PGraphics& pg, float x, float y){ drawPGraphicsRect(pg,x,y,(float)pg.width,(float)pg.height); } void PApplet::image(PGraphics& pg, float x, float y, float w, float h){ drawPGraphicsRect(pg,x,y,w,h); } void PApplet::image(PImage* img, float x, float y) { if(!img || img->width==0 || img->height==0) return; drawImage_impl(img, x, y, (float)img->width, (float)img->height); } void PApplet::image(PImage* img, float x, float y, float w, float h) { if(!img || img->width==0 || img->height==0) return; drawImage_impl(img, x, y, w, h); } void PApplet::imageMode(int m){currentImageMode=m;} void PApplet::tint(float gray) {tint(gray, colorMaxA);} void PApplet::tint(float gray, float a) { // tint(gray, alpha) -- both in 0-255 range like Processing Java tintR = tintG = tintB = gray / 255.f; tintA = a / 255.f; doTint = true; } void PApplet::tint(float r, float g, float b, float a) { // tint(r, g, b, alpha) -- all in 0-255 range tintR = r / 255.f; tintG = g / 255.f; tintB = b / 255.f; tintA = a / 255.f; doTint = true; } void PApplet::noTint(){doTint=false;} void PApplet::filter(int mode) { filter(mode, 0.5f); } void PApplet::filter(int mode, float /*param*/) { int total = winWidth * winHeight; std::vector buf(total * 4); glReadPixels(0, 0, winWidth, winHeight, GL_RGBA, GL_UNSIGNED_BYTE, buf.data()); for (int i = 0; i < total; i++) { int r = buf[i*4], g = buf[i*4+1], b = buf[i*4+2]; int grey = (r + g + b) / 3; if (mode == GRAY) { buf[i*4] = buf[i*4+1] = buf[i*4+2] = (unsigned char)grey; } else if (mode == INVERT) { buf[i*4] = 255 - r; buf[i*4+1] = 255 - g; buf[i*4+2] = 255 - b; } else if (mode == THRESHOLD) { unsigned char t = (grey > 127) ? 255 : 0; buf[i*4] = buf[i*4+1] = buf[i*4+2] = t; } } glDrawPixels(winWidth, winHeight, GL_RGBA, GL_UNSIGNED_BYTE, buf.data()); } void PApplet::loadPixels() { int total = winWidth * winHeight; pixels.resize(total); std::vector rgba(total * 4); glReadPixels(0, 0, winWidth, winHeight, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); // glReadPixels returns rows bottom-to-top; Processing's pixels[] is // top-to-bottom (row 0 = top of window), so flip rows on the way in. for (int y = 0; y < winHeight; y++) { for (int x = 0; x < winWidth; x++) { int di = y * winWidth + x; int si = (winHeight - 1 - y) * winWidth + x; unsigned char r = rgba[si*4 + 0]; unsigned char g = rgba[si*4 + 1]; unsigned char b = rgba[si*4 + 2]; unsigned char a = rgba[si*4 + 3]; pixels[di] = (a << 24) | (r << 16) | (g << 8) | b; // ARGB format } } } void PApplet::updatePixels() { int total = winWidth * winHeight; std::vector rgba(total * 4); // pixels[] is top-to-bottom; glDrawPixels expects bottom-to-top, // so flip rows on the way out. for (int y = 0; y < winHeight; y++) { for (int x = 0; x < winWidth; x++) { int si = y * winWidth + x; int di = (winHeight - 1 - y) * winWidth + x; rgba[di*4 + 0] = (pixels[si] >> 16) & 0xFF; // R rgba[di*4 + 1] = (pixels[si] >> 8) & 0xFF; // G rgba[di*4 + 2] = pixels[si] & 0xFF; // B rgba[di*4 + 3] = (pixels[si] >> 24) & 0xFF; // A } } glDrawPixels(winWidth, winHeight, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); } color PApplet::get(int x, int y) { unsigned char p[4]; glReadPixels(x, winHeight - 1 - y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, p); return colorVal(p[0], p[1], p[2], p[3]); } void PApplet::set(int x, int y, color c) { unsigned int v = c.value; unsigned char p[] = { (unsigned char)((v >> 16) & 0xFF), // R (unsigned char)((v >> 8) & 0xFF), // G (unsigned char)( v & 0xFF), // B (unsigned char)((v >> 24) & 0xFF) // A }; glWindowPos2i(x, winHeight - 1 - y); glDrawPixels(1, 1, GL_RGBA, GL_UNSIGNED_BYTE, p); } // ============================================================================= // BLEND / CLIP // ============================================================================= void PApplet::blendMode(int mode) { glEnable(GL_BLEND); // Reset equation first (SUBTRACT changes it) glBlendEquation(GL_FUNC_ADD); switch (mode) { case ADD: glBlendFunc(GL_SRC_ALPHA, GL_ONE); break; case MULTIPLY: glBlendFunc(GL_DST_COLOR, GL_ZERO); break; case SCREEN: glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); break; case SUBTRACT: glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); glBlendFunc(GL_SRC_ALPHA, GL_ONE); break; default: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; } } void PApplet::clip(float x, float y, float w, float h) { // Scissor coordinates are in GL space (Y=0 at bottom), so flip Y glEnable(GL_SCISSOR_TEST); glScissor((int)x, winHeight - (int)(y + h), (int)w, (int)h); } void PApplet::noClip() { glDisable(GL_SCISSOR_TEST); } // ============================================================================= // SAVE // ============================================================================= void PApplet::saveFrame(const std::string& filename) { std::string fn = filename; size_t pos = fn.find("####"); if (pos != std::string::npos) { char buf[16]; snprintf(buf, sizeof(buf), "%04d", frameCount); fn.replace(pos, 4, buf); } int w = pixelWidth > 0 ? pixelWidth : logicalW; int h = pixelHeight > 0 ? pixelHeight : logicalH; std::vector px(w * h * 3); glPixelStorei(GL_PACK_ALIGNMENT, 1); glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, px.data()); for (int y = 0; y < h / 2; y++) for (int x = 0; x < w * 3; x++) std::swap(px[y*w*3+x], px[(h-1-y)*w*3+x]); std::string ext = fn.size() > 4 ? fn.substr(fn.size()-4) : ""; for (auto& c : ext) c = tolower(c); int ok = 0; if (ext == ".png") ok = stbi_write_png(fn.c_str(), w, h, 3, px.data(), w*3); else if (ext == ".jpg" || ext == "jpeg") ok = stbi_write_jpg(fn.c_str(), w, h, 3, px.data(), 95); else if (ext == ".bmp") ok = stbi_write_bmp(fn.c_str(), w, h, 3, px.data()); else if (ext == ".tga" || ext == ".tif" || ext == ".tiff") ok = stbi_write_tga(fn.c_str(), w, h, 3, px.data()); else { fn += ".png"; ok = stbi_write_png(fn.c_str(), w, h, 3, px.data(), w*3); } if (ok) std::cout << "Saved: " << fn << "\n"; else std::cout << "save: failed to write " << fn << "\n"; } void PApplet::save(const std::string& fn){saveFrame(fn);} // ============================================================================= // GLFW CALLBACKS // ============================================================================= static void cursor_pos_cb(GLFWwindow*, double x, double y) { auto* p = PApplet::g_papplet; if(!p) return; p->mouseInWindow = true; p->mouseDX += (float)x - p->mouseX; p->mouseDY += (float)y - p->mouseY; p->pmouseX = p->mouseX; p->pmouseY = p->mouseY; p->mouseX = (float)x; p->mouseY = (float)y; if (p->_mousePressed) { p->mouseDragged(); } else { p->mouseMoved(); } } static void mouse_btn_cb(GLFWwindow*, int btn, int action, int mods) { auto* p = PApplet::g_papplet; if(!p) return; p->g_currentMods = mods; if (action == GLFW_PRESS) { p->_mousePressed = true; if(btn>=0&&btn<8) p->mouseButtons[btn]=true; { int mb=(btn==GLFW_MOUSE_BUTTON_LEFT)?37:(btn==GLFW_MOUSE_BUTTON_RIGHT)?39:3; if(mb<128) p->mouseDown[mb]=true; } if (btn == GLFW_MOUSE_BUTTON_LEFT) p->mouseButton = LEFT; else if (btn == GLFW_MOUSE_BUTTON_RIGHT) p->mouseButton = RIGHT; else p->mouseButton = CENTER; p->_eventDrewSomething=true; p->_eventDrewSomething=true; p->mousePressed(); p->mouseWasPressed = true; } else if (action == GLFW_RELEASE) { if(btn>=0&&btn<8) p->mouseButtons[btn]=false; { int mb=(btn==GLFW_MOUSE_BUTTON_LEFT)?37:(btn==GLFW_MOUSE_BUTTON_RIGHT)?39:3; if(mb<128) p->mouseDown[mb]=false; } bool anyMouse=false; for(int i=0;i<8;i++) if(p->mouseButtons[i]){anyMouse=true;break;} p->_mousePressed=anyMouse; p->mouseButton = p->mouseButtons[GLFW_MOUSE_BUTTON_LEFT] ? LEFT : p->mouseButtons[GLFW_MOUSE_BUTTON_RIGHT] ? RIGHT : p->mouseButtons[GLFW_MOUSE_BUTTON_MIDDLE] ? CENTER : -1; p->mouseReleased(); if(p->mouseWasPressed) p->mouseClicked(); p->mouseWasPressed=false; } } static void scroll_cb(GLFWwindow*,double,double yoffset){ auto* p = PApplet::g_papplet; if(!p) return; if(p->_onMouseWheel)p->_onMouseWheel((int)yoffset); } static void char_cb(GLFWwindow*, unsigned int codepoint) { auto* p = PApplet::g_papplet; if(!p) return; // char_cb fires after key_cb for printable keys, with the correct // Unicode character (shift/caps/layout all applied). if (codepoint < 128) { p->key = (char16_t)codepoint; } // Fire deferred keyPressed() now that p->key has the correct char value if (p->g_pendingKeyPressed) { p->g_pendingKeyPressed = false; p->_eventDrewSomething=true; p->keyPressed(); } // keyTyped() -- Processing standard: only printable chars, no action keys // Action keys (Ctrl, Shift, Alt, etc.) never reach char_cb, so this is correct. p->keyTyped(); } // Translate GLFW key code to Java KeyEvent.VK_* value. // The Processing reference says keyCode matches Java's KeyEvent constants. static int glfw_to_java_keycode(int k) { switch (k) { // Arrow / navigation case GLFW_KEY_UP: return 38; case GLFW_KEY_DOWN: return 40; case GLFW_KEY_LEFT: return 37; case GLFW_KEY_RIGHT: return 39; case GLFW_KEY_HOME: return 36; case GLFW_KEY_END: return 35; case GLFW_KEY_PAGE_UP: return 33; case GLFW_KEY_PAGE_DOWN: return 34; // Modifiers case GLFW_KEY_LEFT_SHIFT: case GLFW_KEY_RIGHT_SHIFT: return 16; case GLFW_KEY_LEFT_CONTROL: case GLFW_KEY_RIGHT_CONTROL: return 17; case GLFW_KEY_LEFT_ALT: case GLFW_KEY_RIGHT_ALT: return 18; case GLFW_KEY_LEFT_SUPER: case GLFW_KEY_RIGHT_SUPER: return 157; // VK_META // ASCII control (key is set directly, not CODED, but keyCode is still set) case GLFW_KEY_BACKSPACE: return 8; case GLFW_KEY_TAB: return 9; case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: return 10; // ENTER (also fires RETURN=13 via key) case GLFW_KEY_ESCAPE: return 27; case GLFW_KEY_DELETE: return 127; case GLFW_KEY_INSERT: return 155; // Function keys case GLFW_KEY_F1: return 112; case GLFW_KEY_F2: return 113; case GLFW_KEY_F3: return 114; case GLFW_KEY_F4: return 115; case GLFW_KEY_F5: return 116; case GLFW_KEY_F6: return 117; case GLFW_KEY_F7: return 118; case GLFW_KEY_F8: return 119; case GLFW_KEY_F9: return 120; case GLFW_KEY_F10: return 121; case GLFW_KEY_F11: return 122; case GLFW_KEY_F12: return 123; // Letter keys: GLFW A=65..Z=90 matches Java VK_A=65..VK_Z=90 // (GLFW uses uppercase, Java uses uppercase -- same values) default: if (k >= GLFW_KEY_A && k <= GLFW_KEY_Z) return k; // 65..90 match if (k >= GLFW_KEY_0 && k <= GLFW_KEY_9) return k - GLFW_KEY_0 + 48; // 48..57 return k; // best effort for anything else } } // Current modifier state -- set from key_cb mods parameter (reliable on Windows) // Map GLFW keycode -> Processing/Java keyCode constant static int glfw_to_processing_keycode(int k) { if(k >= GLFW_KEY_A && k <= GLFW_KEY_Z) return 'A' + (k - GLFW_KEY_A); // 65-90 if(k >= GLFW_KEY_0 && k <= GLFW_KEY_9) return '0' + (k - GLFW_KEY_0); // 48-57 switch(k) { case GLFW_KEY_UP: return 38; case GLFW_KEY_DOWN: return 40; case GLFW_KEY_LEFT: return 37; case GLFW_KEY_RIGHT: return 39; case GLFW_KEY_LEFT_SHIFT: case GLFW_KEY_RIGHT_SHIFT: return 16; // SHIFT case GLFW_KEY_LEFT_CONTROL: case GLFW_KEY_RIGHT_CONTROL:return 17; // CONTROL case GLFW_KEY_LEFT_ALT: case GLFW_KEY_RIGHT_ALT: return 18; // ALT case GLFW_KEY_SPACE: return 32; case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: return 10; case GLFW_KEY_BACKSPACE: return 8; case GLFW_KEY_TAB: return 9; case GLFW_KEY_ESCAPE: return 27; case GLFW_KEY_DELETE: return 127; case GLFW_KEY_HOME: return 36; case GLFW_KEY_END: return 35; case GLFW_KEY_PAGE_UP: return 33; case GLFW_KEY_PAGE_DOWN: return 34; case GLFW_KEY_INSERT: return 155; case GLFW_KEY_F1: return 112; case GLFW_KEY_F2: return 113; case GLFW_KEY_F3: return 114; case GLFW_KEY_F4: return 115; case GLFW_KEY_F5: return 116; case GLFW_KEY_F6: return 117; case GLFW_KEY_F7: return 118; case GLFW_KEY_F8: return 119; case GLFW_KEY_F9: return 120; case GLFW_KEY_F10: return 121; case GLFW_KEY_F11: return 122; case GLFW_KEY_F12: return 123; default: return -1; } } static void key_cb(GLFWwindow* w, int k, int /*scancode*/, int action, int mods) { auto* p = PApplet::g_papplet; if(!p) return; p->g_currentMods = mods; // capture before callbacks fire if (action == GLFW_PRESS || action == GLFW_REPEAT) { p->_keyPressed = true; if(k>=0&&k<349) p->keys[k]=true; { int pk=glfw_to_processing_keycode(k); if(pk>=0&&pk<256) { p->keysDown[pk]=true; // Also store lowercase so K['w'] and K['W'] both work if(pk>='A'&&pk<='Z') p->keysDown[pk+32]=true; if(pk>='a'&&pk<='z') p->keysDown[pk-32]=true; } } p->g_pendingKeyPressed = false; // reset for each new p->key event // Translate GLFW code -> Java KeyEvent.VK_* value (Processing reference standard) p->keyCode = glfw_to_java_keycode(k); // Set p->key=CODED for all non-printable/special keys. // ASCII keys that Processing handles via p->key directly (not CODED): // BACKSPACE(8), TAB(9), ENTER(10), ESC(27), DELETE(127) // Everything else that has no ASCII representation gets CODED. switch (k) { case GLFW_KEY_BACKSPACE: p->key = (char16_t)8; break; case GLFW_KEY_TAB: p->key = (char16_t)9; break; case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: p->key = (char16_t)10; break; case GLFW_KEY_ESCAPE: p->key = (char16_t)27; break; case GLFW_KEY_SPACE: p->key = (char16_t)32; break; case GLFW_KEY_DELETE: p->key = (char16_t)127; break; // All other special keys set p->key=CODED case GLFW_KEY_UP: case GLFW_KEY_DOWN: case GLFW_KEY_LEFT: case GLFW_KEY_RIGHT: case GLFW_KEY_HOME: case GLFW_KEY_END: case GLFW_KEY_PAGE_UP: case GLFW_KEY_PAGE_DOWN: case GLFW_KEY_LEFT_SHIFT: case GLFW_KEY_RIGHT_SHIFT: case GLFW_KEY_LEFT_CONTROL: case GLFW_KEY_RIGHT_CONTROL: case GLFW_KEY_LEFT_ALT: case GLFW_KEY_RIGHT_ALT: case GLFW_KEY_LEFT_SUPER: case GLFW_KEY_RIGHT_SUPER: case GLFW_KEY_INSERT: case GLFW_KEY_CAPS_LOCK: case GLFW_KEY_F1: case GLFW_KEY_F2: case GLFW_KEY_F3: case GLFW_KEY_F4: case GLFW_KEY_F5: case GLFW_KEY_F6: case GLFW_KEY_F7: case GLFW_KEY_F8: case GLFW_KEY_F9: case GLFW_KEY_F10: case GLFW_KEY_F11: case GLFW_KEY_F12: p->key = CODED; // no (char) cast -- key is now int, casting to char would truncate 0xFFFF back down to the original bug break; default: // Printable p->key: if a modifier (Ctrl/Alt) is held, char_cb will // NOT fire on Windows for Ctrl+letter combos. Fire immediately. // If no modifier, defer to char_cb so p->key gets the correct char. if (mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER)) { // Ctrl/Alt/Meta combos: char_cb won't fire. // Set p->key to the lowercase letter and fire immediately. if (k >= GLFW_KEY_A && k <= GLFW_KEY_Z) p->key = (char16_t)('a' + (k - GLFW_KEY_A)); p->g_pendingKeyPressed = false; // fire immediately } else { // Plain printable p->key (possibly with Shift): defer to char_cb // so p->key gets the correct shifted/layout-aware character. p->g_pendingKeyPressed = true; } break; } // Fire keyPressed() now unless deferred to char_cb if (!p->g_pendingKeyPressed) { if(action==GLFW_PRESS) p->_eventDrewSomething=true; p->keyPressed(); // Java Processing: ESC closes the sketch unless keyPressed() set p->key=0 if (p->key == (char16_t)27 && p->gWindow) glfwSetWindowShouldClose(p->gWindow, GLFW_TRUE); } } else if (action == GLFW_RELEASE) { if(k>=0&&k<349) p->keys[k]=false; { int pk=glfw_to_processing_keycode(k); if(pk>=0&&pk<256) { p->keysDown[pk]=false; if(pk>='A'&&pk<='Z') p->keysDown[pk+32]=false; if(pk>='a'&&pk<='z') p->keysDown[pk-32]=false; } } // check if any key still held bool anyHeld=false; for(int i=0;i<349;i++) if(p->keys[i]){anyHeld=true;break;} p->_keyPressed=anyHeld; p->g_currentMods = mods; // update on release too p->keyReleased(); } } static void focus_cb(GLFWwindow*,int f){ auto* p = PApplet::g_papplet; if(!p) return; p->focused = (f == GLFW_TRUE); if (!p->focused) p->g_pendingKeyPressed = false; // clear pending if focus lost } static void winpos_cb(GLFWwindow*,int,int){ auto* p = PApplet::g_papplet; if(!p) return;if(p->_onWindowMoved)p->_onWindowMoved();} static void winsize_cb(GLFWwindow*,int lw,int lh){ auto* p = PApplet::g_papplet; if(!p) return; if(!lw||!lh||p->_inWinsizeCb)return; p->_inWinsizeCb = true; { int fbw=0, fbh=0; if (p->gWindow) glfwGetFramebufferSize(p->gWindow, &fbw, &fbh); PDEBUG("winsize_cb FIRED: callback-reported lw=%d lh=%d | isResizable=%d setupDone=%d\n", lw, lh, p->isResizable, p->_setupDone); PDEBUG(" BEFORE update: logicalW=%d logicalH=%d winWidth=%d winHeight=%d realFbSize=(%d,%d)\n", p->logicalW, p->logicalH, p->winWidth, p->winHeight, fbw, fbh); } if(p->_setupDone){ if(p->isResizable){ // Update logical size for resizable sketches so width/height reflect new size p->winWidth=lw; p->winHeight=lh; p->logicalW=lw; p->logicalH=lh; } else { PDEBUG(" isResizable=false -- logicalW/logicalH NOT updated despite a real " "window-size-change event (lw=%d lh=%d). If the OS/WM forced an actual " "geometry change anyway (common with tiling WMs like i3, which can " "override an app's requested size), logicalW/H now silently disagree " "with the window's REAL size -- everything drawn using logical " "coordinates will appear scaled/contained relative to the actual " "window, since the viewport tracks real size but the projection still " "uses the stale logical one.\n"); } if(p->_onWindowResized)p->_onWindowResized(); } { int fbw=0, fbh=0; if (p->gWindow) glfwGetFramebufferSize(p->gWindow, &fbw, &fbh); PDEBUG(" AFTER update: logicalW=%d logicalH=%d winWidth=%d winHeight=%d realFbSize=(%d,%d)\n", p->logicalW, p->logicalH, p->winWidth, p->winHeight, fbw, fbh); } // p->setProjection uses p->logicalW/H for ortho, actual size for viewport. p->setProjection(p->winWidth, p->winHeight); // Only trigger redraw if size actually changed from last draw. // This prevents flashing from rapid i3 resize events. static int _lastDrawW = 0, _lastDrawH = 0; if(p->_setupDone && !p->looping) { if(lw != _lastDrawW || lh != _lastDrawH) { _lastDrawW = lw; _lastDrawH = lh; p->redrawOnce = true; } } p->_inWinsizeCb = false; } static void fbsize_cb(GLFWwindow*,int fw,int fh){ auto* p = PApplet::g_papplet; if(!p) return; if(!fw||!fh)return; p->pixelWidth=fw;p->pixelHeight=fh; } // ============================================================================= // RUN // ============================================================================= // Called from main() before run() if --debug flag is present void PApplet::enableDebugConsole(){_doEnableDebugConsole();} void PApplet::run(){ PDEBUG("CppMode build stamp: %s\n", PROCESSING_BUILD_STAMP); PDEBUG("Debug mode ON -- PApplet::run() starting\n"); g_papplet = this; signal(SIGTERM, [](int){ if(PApplet::g_papplet && PApplet::g_papplet->gWindow) glfwSetWindowShouldClose(PApplet::g_papplet->gWindow, GLFW_TRUE); }); #ifndef _WIN32 // SIGHUP is POSIX-only -- Windows has no controlling-terminal/session // concept, so there's no equivalent signal to catch here at all. signal(SIGHUP, [](int){ if(PApplet::g_papplet && PApplet::g_papplet->gWindow) glfwSetWindowShouldClose(PApplet::g_papplet->gWindow, GLFW_TRUE); }); #endif // Write directly to a file since -mwindows kills stderr on Windows setvbuf(stdout, nullptr, _IONBF, 0); std::srand((unsigned)std::time(nullptr)); initPerlin(0); // initialize noise table with default seed if(!glfwInit()){ fprintf(stderr, "[ERR] glfwInit() failed. Make sure libglfw3.dll is next to ide.exe\n"); #ifdef _WIN32 MessageBoxA(NULL, "glfwInit() failed.\nMake sure libglfw3.dll and glew32.dll are next to ide.exe", "processing-cpp Error", MB_OK|MB_ICONERROR); #endif return; } GLFWmonitor* mon=glfwGetPrimaryMonitor(); if(mon){const GLFWvidmode* vm=glfwGetVideoMode(mon);displayWidth=vm->width;displayHeight=vm->height;} // Reset all style state to defaults BEFORE this->setup() doFill=true;doStroke=true; fillR=1;fillG=1;fillB=1;fillA=1; strokeR=0;strokeG=0;strokeB=0;strokeA=1;strokeW=1; colorModeVal=RGB; colorMaxH=255;colorMaxS=255;colorMaxB=255;colorMaxA=255; currentRectMode=CORNER;currentEllipseMode=CENTER;currentImageMode=CORNER; doTint=false;tintR=1;tintG=1;tintB=1;tintA=1; lightsEnabled=false;lightIndex=0; looping=true; this->settings(); glfwWindowHint(GLFW_RESIZABLE,isResizable?GLFW_TRUE:GLFW_FALSE); glfwWindowHint(GLFW_SAMPLES,4); // 4x MSAA for crisp P3D rendering; 2D noSmooth() disables at runtime glfwWindowHint(GLFW_STENCIL_BITS,8); // needed for concave shape fill gWindow=glfwCreateWindow(winWidth,winHeight,g_sketchName.c_str(),nullptr,nullptr); // Prevent freeze when dragging title bar on Windows glfwSetWindowRefreshCallback(gWindow,[](GLFWwindow* w){ // Originally just glClear()+swap "to prevent freeze when dragging the // title bar on Windows" -- but this unconditionally WIPES the screen // on every OS/WM-triggered refresh event (resize, focus change, // workspace switch, tiling re-layout, etc.), with no attempt to // redraw actual sketch content. On tiling WMs like i3 that send // refresh events frequently, this is what made noLoop() sketches // (which never swap again after their one real draw) appear to // "show the image, then go blank a few frames later" -- the WM's // very next refresh event was clearing it out from under them. // Restoring from the persist FBO (the last frame we know is // correct) instead of blank-clearing keeps the window responsive // during the same blocking scenarios (e.g. title-bar drag) while // never destroying real content. auto* p = PApplet::g_papplet; if (p && p->persistFBO) { p->restoreFromPersist(); } else { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); } glfwSwapBuffers(w); }); if(!gWindow){ #ifdef _WIN32 MessageBoxA(NULL, "Window creation failed.\nCheck that your GPU supports OpenGL 2.0+", "processing-cpp Error", MB_OK|MB_ICONERROR); #else fprintf(stderr, "[ERR] Window creation failed. Check OpenGL 2.0+ support.\n"); #endif glfwTerminate(); return; } glfwMakeContextCurrent(gWindow); GLenum glewErr = glewInit(); if(glewErr != GLEW_OK){ #ifdef _WIN32 char msg[256]; snprintf(msg,sizeof(msg),"glewInit() failed: %s", glewGetErrorString(glewErr)); MessageBoxA(NULL, msg, "processing-cpp Error", MB_OK|MB_ICONERROR); #else fprintf(stderr, "[ERR] glewInit() failed: %s\n", glewGetErrorString(glewErr)); #endif glfwDestroyWindow(gWindow); glfwTerminate(); return; } // Reset cached Phong shader so it recompiles fresh with current source if(phongProg){glDeleteProgram(phongProg);phongProg=0;} glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); glEnable(GL_NORMALIZE); smooth(); // Don't call setProjection here -- let size() in this->setup() do it // with the correct dimensions. Calling it now with winWidth=640,winHeight=480 // (defaults) would set the wrong ortho before this->setup() changes the size. {int fw,fh;glfwGetFramebufferSize(gWindow,&fw,&fh);pixelWidth=fw;pixelHeight=fh;fbW=fw>0?fw:logicalW;fbH=fh>0?fh:logicalH;} // Enable sticky keys/buttons: GLFW will keep state as PRESSED until polled, // Clear both buffers once at startup so the sketch starts with // a known clean state (no GPU garbage in either buffer). glClearColor(0.8f,0.8f,0.8f,1); // Java Processing default grey glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glfwSwapBuffers(gWindow); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // preventing missed inputs on Windows where events can arrive between polls. glfwSetInputMode(gWindow, GLFW_STICKY_KEYS, GLFW_TRUE); glfwSetInputMode(gWindow, GLFW_STICKY_MOUSE_BUTTONS, GLFW_TRUE); glfwSetCursorPosCallback(gWindow, cursor_pos_cb); glfwSetMouseButtonCallback(gWindow, mouse_btn_cb); glfwSetScrollCallback(gWindow, scroll_cb); glfwSetKeyCallback(gWindow, key_cb); glfwSetCharCallback(gWindow, char_cb); glfwSetFramebufferSizeCallback(gWindow,fbsize_cb); glfwSetWindowSizeCallback(gWindow, winsize_cb); glfwSetWindowFocusCallback(gWindow, focus_cb); glfwSetWindowPosCallback(gWindow, winpos_cb); focused=(glfwGetWindowAttrib(gWindow,GLFW_FOCUSED)==GLFW_TRUE); // Auto-load default.ttf from project root as the default font // (matches Processing Java's "a generic sans-serif font will be used") // Try to load default.ttf from several common locations { std::string _homeDir; #ifdef _WIN32 if (const char* h = std::getenv("USERPROFILE")) _homeDir = h; #else if (const char* h = std::getenv("HOME")) _homeDir = h; #endif std::string _modePath; if (const char* mp = std::getenv("PROCESSING_MODE_PATH")) _modePath = std::string(mp) + "/"; // Font name used by Processing4 const std::string _font = "ProcessingSansPro-Regular.ttf"; // Check Documents/Processing on Windows (user sketchbook) std::string _docsPath; #ifdef _WIN32 if (const char* ud = std::getenv("USERPROFILE")) _docsPath = std::string(ud) + "/Documents/Processing/"; #endif if (!tryLoadTTF(_modePath + "fonts/" + _font, g_textSize) && !tryLoadTTF(_modePath + _font, g_textSize) && !tryLoadTTF(_docsPath + "modes/CppMode/fonts/" + _font, g_textSize) && // Windows: Processing4 install locations !tryLoadTTF("C:/Program Files/Processing/core/library/" + _font, g_textSize) && !tryLoadTTF("C:/Program Files (x86)/Processing/core/library/" + _font, g_textSize) && !tryLoadTTF("C:/Program Files/processing-4.3/core/library/" + _font, g_textSize) && !tryLoadTTF(_homeDir + "/AppData/Local/Programs/Processing/core/library/" + _font, g_textSize) && // Windows: dev build !tryLoadTTF(_homeDir + "/Projects/processing4/core/src/font/" + _font, g_textSize) && // Linux: installed Processing4 !tryLoadTTF("/usr/lib/processing4/core/library/" + _font, g_textSize) && !tryLoadTTF("/usr/share/processing4/core/library/" + _font, g_textSize) && !tryLoadTTF("/opt/processing4/core/library/" + _font, g_textSize) && !tryLoadTTF("/opt/processing/core/library/" + _font, g_textSize) && !tryLoadTTF(_homeDir + "/.local/share/processing4/core/library/" + _font, g_textSize) && !tryLoadTTF(_homeDir + "/processing-4.3/core/library/" + _font, g_textSize) && !tryLoadTTF(_homeDir + "/Projects/processing4/core/src/font/" + _font, g_textSize) && // macOS !tryLoadTTF("/Applications/Processing.app/Contents/Java/core/library/" + _font, g_textSize) && !tryLoadTTF(_homeDir + "/Applications/Processing.app/Contents/Java/core/library/" + _font, g_textSize)) { std::cerr << "[font] ProcessingSansPro-Regular.ttf not found -- using bitmap fallback\n"; } } glfwFocusWindow(gWindow); // ensure input focus on Windows // Settle loop: poll+swap several times BEFORE this->setup() runs so i3/tiling WMs // fully resize the window first. winsize_cb fires during these polls and // sets the correct viewport. After settling, this->setup() and this->draw() use the // correct dimensions from the start -- no shifted first frame. _setupDone = true; // enable winsize_cb to update projection during settle // Initialize the framebuffer before the settle loop -- required on Windows // to avoid swapping an uninitialized back buffer which crashes the driver. glClearColor(0.8f,0.8f,0.8f,1); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glfwSwapBuffers(gWindow); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glfwSwapBuffers(gWindow); for (int _settle = 0; _settle < 5; _settle++) { glClearColor(0.8f,0.8f,0.8f,1); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glfwPollEvents(); glfwSwapBuffers(gWindow); } if (!defaultP3D) setProjection(winWidth, winHeight); this->setup(); // Real Processing's actual default is smooth(2) -- antialiased -- // even for plain 2D sketches (P2D/P3D both default to smooth(2); the // legacy JAVA2D software renderer defaults to smooth(3)/bicubic). // This previously UNCONDITIONALLY disabled MSAA for every 2D sketch, // on the assumption that 2D wants "crisp" pixels by default -- that's // backwards from real Processing's actual behavior, and it's why // PGraphics buffers (and the main canvas) never looked antialiased // even after wiring up multisample framebuffers: GL_MULTISAMPLE was // globally disabled the whole time regardless of framebuffer setup. // Only disable smoothing if the sketch explicitly called noSmooth() // (smoothing==false) BEFORE this point -- otherwise leave the // defaults smooth()/setup() already enabled (smooth() runs earlier in // PApplet::run(), before this->setup(), as part of normal startup). if (!defaultP3D && !smoothing) { glDisable(GL_MULTISAMPLE); glDisable(GL_POINT_SMOOTH); glDisable(GL_LINE_SMOOTH); } if (_wireCallbacksFn) _wireCallbacksFn(); if (!defaultP3D) { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } // If this->setup() called noLoop(): run this->draw() once then swap. // Static sketches have an empty this->draw() -- no-op. // Structured noLoop sketches (like Pie Chart) need this->draw() to show content. if (!looping) { if (defaultP3D) { // P3D static sketch: this->setup() already drew everything with the correct // camera. Just apply projection state for the this->draw() call (which may // be empty for static sketches, or may draw content for structured ones). glViewport(0, 0, fbW, fbH); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glDisable(GL_CULL_FACE); glFrontFace(GL_CW); glEnable(GL_NORMALIZE); // Do NOT clear -- this->setup() already drew to the back buffer. // Only clear if this->draw() will redraw everything (i.e. calls background()). applyDefaultCamera(); } else { setProjection(logicalW, logicalH); } ++frameCount; this->draw(); mouseDX = 0; mouseDY = 0; // reset delta each frame flushPoints(); // flush any pending points before swap saveToPersist(); // save back buffer before swap for next frame restore glfwSwapBuffers(gWindow); // For noLoop() sketches, this is the ONLY draw that will ever happen -- // the main loop below never swaps again (looping||redrawOnce stays // false forever after this point). With double buffering, a single // swap leaves the correct image in only ONE of the two buffers; the // other buffer still holds stale content from the settle loop above. // If anything outside our control (window manager/compositor on // tiling WMs like i3, focus changes, etc.) forces an implicit buffer // flip later, that stale buffer becomes visible and the sketch // appears to "go blank" after a few frames. Restoring from the // persist FBO we just saved and swapping again primes BOTH buffers // with the correct content, so no swap-parity assumption is needed. restoreFromPersist(); glfwSwapBuffers(gWindow); } redrawOnce = looping; auto last=std::chrono::steady_clock::now(); // Drain Windows message queue before entering main loop. // On Windows, a WM_QUIT from a previous sketch run can be in the queue. // Poll multiple times to flush it, then forcibly clear the close flag. for(int _flush=0; _flush<10; _flush++) glfwPollEvents(); glfwSetWindowShouldClose(gWindow, GLFW_FALSE); // For looping sketches: clear both buffers to bgColor so the // front-to-back blit starts from the correct background color. // For static sketches (noLoop): don't clear - preserve what this->setup() drew. if(looping){ for(int _b=0;_b<2;_b++){ glClearColor(bgR,bgG,bgB,bgA); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glfwSwapBuffers(gWindow); } } while(!glfwWindowShouldClose(gWindow)){ if(looping||redrawOnce){ redrawOnce=false; if(defaultP3D){ glViewport(0, 0, fbW, fbH); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glDisable(GL_CULL_FACE); glFrontFace(GL_CW); glEnable(GL_NORMALIZE); // Do NOT clear color here -- the sketch calls background() itself. // Clearing color would erase anything drawn in this->setup(). flushPoints(); // flush any pending points from previous frame glClear(GL_DEPTH_BUFFER_BIT); // Auto-apply the default Processing camera BEFORE this->draw() -- // matches Java Processing behaviour exactly. The sketch can // override by calling camera() / perspective() itself. // This means lights() called anywhere in this->draw() will always // see the camera matrix and lock correctly in eye space. applyDefaultCamera(); } else { // 2D mode: reset viewport and projection every frame. // Use actual framebuffer size for viewport (handles HiDPI/Retina // where framebuffer may be 2x logical size) but logical // winWidth/winHeight for the ortho matrix so sketch pixel // coordinates map 1:1 regardless of DPI scaling. // // BUG FIX: glLoadIdentity() alone only resets the CONTENT of // whatever's currently on top of the matrix stack -- it does // nothing about the stack's DEPTH. If a previous frame left // an unbalanced glPushMatrix() somewhere (e.g. a PGraphics // buffer's beginDraw() that was abandoned without a matching // endDraw(), which can happen if a sketch reassigns a // PGraphics* pointer to a new buffer without ever calling // endDraw()/delete on the old one), every subsequent // glLoadIdentity() still operates one level too deep on the // stack, and main-canvas drawing for the REST OF THE PROGRAM // silently inherits that leftover transform -- exactly // matching "the same line(...) call lands in a different // place depending on what ran before it." Fully unwinding // the stack to its base depth every frame, before doing // anything else, makes the main render loop resilient to // ANY code (ours or the sketch's) that fails to balance its // own push/pop calls, rather than silently accumulating // corruption frame after frame. { // Pop down to stack depth 1 (the base level) rather // than a blind fixed-count loop -- popping past the // bottom of the stack is ALSO undefined behavior, // exactly the same category of bug as overflowing it, // so we query the actual current depth first. GLint depth = 0; glMatrixMode(GL_PROJECTION); glGetIntegerv(GL_PROJECTION_STACK_DEPTH, &depth); while (depth > 1) { glPopMatrix(); depth--; } glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glGetIntegerv(GL_MODELVIEW_STACK_DEPTH, &depth); while (depth > 1) { glPopMatrix(); depth--; } glLoadIdentity(); } glViewport(0, 0, fbW, fbH); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, logicalW, logicalH, 0, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); // Restore previous frame content from persist FBO. // Sketches that call background() will overwrite this. // Sketches that don't (like Continuous Lines) preserve their drawn content. if(!_backgroundCalledThisFrame) restoreFromPersist(); glClear(GL_DEPTH_BUFFER_BIT); } // Reset all light slots each frame so lights from the previous // frame don't accumulate. The sketch re-establishes its lights // in each this->draw() call. for (int _li = 0; _li < 8; _li++) { glDisable(GL_LIGHT0 + _li); // Reset each slot to black so disabled lights contribute nothing GLfloat black[] = { 0.f, 0.f, 0.f, 1.f }; glLightfv(GL_LIGHT0+_li, GL_DIFFUSE, black); glLightfv(GL_LIGHT0+_li, GL_AMBIENT, black); glLightfv(GL_LIGHT0+_li, GL_SPECULAR, black); } glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); // No global ambient by default (Java Processing uses 0 unless ambientLight() called) GLfloat noAmb[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, noAmb); lightsEnabled = false; lightIndex = 0; pendingSpecR=0; pendingSpecG=0; pendingSpecB=0; for(int _li=0;_li<8;_li++){lightConcentration[_li]=0;lightCutoffCos[_li]=-1;} // Reset material to neutral so non-lit objects look correct GLfloat matWhite[] = { 0.8f, 0.8f, 0.8f, 1.0f }; GLfloat matBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, matBlack); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, matWhite); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, matBlack); glMaterialf (GL_FRONT_AND_BACK, GL_SHININESS, 0.0f); // Reset specular material to none GLfloat noSpec[] = {0,0,0,1}; glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, noSpec); glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, noSpec); // Draw guard: handle different sketch types if (!looping && _staticSketchSetup) { // Static sketch: reset style and redraw doFill=true; doStroke=true; fillR=1; fillG=1; fillB=1; fillA=1; strokeR=0; strokeG=0; strokeB=0; strokeA=1; strokeW=1; colorModeVal=RGB; colorMaxH=255; colorMaxS=255; colorMaxB=255; colorMaxA=255; lightsEnabled=false; lightIndex=0; _staticSketchSetup(); } else { // Run this->draw() every frame -- no mouseInWindow guard. // The Hue sketch first-frame issue is handled by the settle loop // ensuring mouseX/mouseY are 0 which is acceptable. // Reset tint state each frame (Processing Java behavior) doTint=false; tintR=1; tintG=1; tintB=1; tintA=1; glGetError(); _backgroundCalledThisFrame = false; ++frameCount; this->draw(); pmouseX=mouseX; pmouseY=mouseY; mouseDX=0; mouseDY=0; glGetError(); // consume any GL errors } // Only save persist FBO if sketch uses persistence (no background() call) if(!_backgroundCalledThisFrame) saveToPersist(); glfwSwapBuffers(gWindow); } glfwPollEvents(); // Only re-save persist if an event callback drew something. // Checked via _eventDrewSomething flag set in keyPressed/mousePressed. if(persistFBO && _eventDrewSomething) { saveToPersist(); _eventDrewSomething=false; } // Frame timing -- matches Processing Java's model exactly: // sleep the remainder of the target frame time, then measure // the full wall-clock frame duration (draw + swap + sleep) and // report it as _frameRate. auto now = std::chrono::steady_clock::now(); double elapsed = std::chrono::duration(now - last).count(); double sl = targetFrameTime - elapsed; if (sl > 0) std::this_thread::sleep_for(std::chrono::duration(sl)); auto frameEnd = std::chrono::steady_clock::now(); double fullFrame = std::chrono::duration(frameEnd - last).count(); if (fullFrame > 0) _frameRate = (float)(1.0 / fullFrame); last = frameEnd; } if(phongProg){glDeleteProgram(phongProg);phongProg=0;} glfwDestroyWindow(gWindow);gWindow=nullptr;glfwTerminate(); } // ============================================================================= // JSON IMPLEMENTATION // ============================================================================= static void skipWS(const std::string& s, size_t& i){ while(i=s.size()) return JSONValue(); if(s[i]=='"') return JSONValue(parseString(s,i)); if(s[i]=='{') return JSONValue(parseJSONObj(s,i)); if(s[i]=='[') return JSONValue(parseJSONArr(s,i)); if(s.substr(i,4)=="null") { i+=4; return JSONValue(); } if(s.substr(i,4)=="true") { i+=4; return JSONValue(true); } if(s.substr(i,5)=="false"){ i+=5; return JSONValue(false); } size_t start=i; bool isFloat=false; if(iempty())return "[]"; std::string r="[\n"; for(size_t i=0;isize();i++){ r+=pad2+toJSONString((*v.arr)[i],indent+1); if(i+1size())r+=","; r+="\n"; } return r+pad+"]"; } case JSONValue::OBJECT_T: { if(v.obj->empty())return "{}"; std::string r="{\n"; size_t n=0; for(auto& p:*v.obj){ r+=pad2+"\""+p.first+"\": "+toJSONString(p.second,indent+1); if(++nsize())r+=","; r+="\n"; } return r+pad+"}"; } } return "null"; } static std::string readFileString(const std::string& path){ std::ifstream f(path); if(!f)return ""; return std::string((std::istreambuf_iterator(f)),std::istreambuf_iterator()); } JSONValue PApplet::loadJSONObject(const std::string& path){ return parseJSON(readFileString(path)); } JSONValue PApplet::loadJSONArray(const std::string& path) { return parseJSON(readFileString(path)); } bool PApplet::saveJSONObject(const std::string& path,const JSONValue& v,int indent){ std::ofstream f(path); if(!f)return false; f<=s.size()||s[i]!='<')return node; i++; if(i'||s.substr(i,2)=="?>"))i++; if(i')i++; i++; return node; } while(i'&&s[i]!='/')node.name+=s[i++]; xmlSkipWS(s,i); while(i'&&s[i]!='/'){ std::string key; while(i'&&s[i]!='/')key+=s[i++]; xmlSkipWS(s,i); if(i')i++; if(i\n";return r;} r+=">"; if(!content.empty())r+=content; if(!children.empty()){r+="\n";for(auto& c:children)r+=c.toString(indent+1);r+=pad;} r+=""+name+">\n"; return r; } bool PApplet::saveXML(const std::string& path,const XML& x){ std::ofstream f(path);if(!f)return false;f<addColumn(c); start=1; } for(int i=start;i<(int)lines.size();i++){ auto& row=t->addRow(); auto cells=split(lines[i],delim); for(int j=0;j<(int)cells.size()&&j<(int)row.size();j++)row[j]=cells[j]; } return t; } bool PApplet::saveTable(const std::string& path,const Table& t,const std::string& ext){ std::ofstream f(path);if(!f)return false; char delim=ext=="tsv"?'\t':','; if(!t.columns.empty()){for(size_t i=0;ii&&(s[j-1]=='e'||s[j-1]=='E'))))j++; float v=0; try{v=std::stof(s.substr(i,j-i));}catch(...){} i=j; return v; } static uint32_t svgParseColor(const std::string& s){ if(s.empty()||s=="none") return 0; if(s[0]=='#'){ std::string h=s.substr(1); if(h.size()==3)h={h[0],h[0],h[1],h[1],h[2],h[2]}; uint32_t v=0; try{v=(uint32_t)std::stoul(h,nullptr,16);}catch(...){} return v|0xFF000000u; } // Named colors (common subset) static const std::unordered_map<:string> nc={ {"black",0xFF000000},{"white",0xFFFFFFFF},{"red",0xFF0000FF}, {"green",0xFF008000},{"blue",0xFF0000FF},{"none",0}, {"gray",0xFF808080},{"grey",0xFF808080},{"yellow",0xFFFFFF00}, }; auto it=nc.find(s); return it!=nc.end()?it->second:0xFF000000; } static std::string svgAttr(const std::string& tag, const std::string& attr){ // Find attr="value" -- whole-word match to avoid "id=" matching "d=" size_t p=0; std::string needle=attr+"="; while(true){ p=tag.find(needle,p); if(p==std::string::npos) return ""; // Must be preceded by space, tab, or start (whole attribute name) bool ok=(p==0||tag[p-1]==' '||tag[p-1]==9||tag[p-1]==58); // 58=':' if(ok) break; p++; } p+=needle.size(); if(p>=tag.size()) return ""; char q=tag[p++]; if(q!=34&&q!=39) return ""; // 34='"', 39="'" size_t e=tag.find(q,p); return e==std::string::npos?tag.substr(p):tag.substr(p,e-p); } static void svgApplyStyle(PShape& sh, const std::string& tag){ // Parse style="..." or individual fill/stroke/stroke-width attrs std::string style=svgAttr(tag,"style"); auto getVal=[&](const std::string& k)->std::string{ // from inline style size_t p=style.find(k+":"); if(p!=std::string::npos){ p+=k.size()+1; while(p>16)&0xFF)/255.f,((c>>8)&0xFF)/255.f,(c&0xFF)/255.f,falpha); sh.hasFill=true; } else if(fs=="none"){sh.hasFill=false;} if(!ss.empty()&&ss!="none"){ uint32_t c=svgParseColor(ss); sh.setStroke(((c>>16)&0xFF)/255.f,((c>>8)&0xFF)/255.f,(c&0xFF)/255.f,alpha); sh.hasStroke=true; } if(!sw.empty())try{sh.strokeW=std::stof(sw);}catch(...){} } // Parse SVG path 'd' attribute into vertices static PShape svgParsePath(const std::string& tag){ PShape sh; sh.kind=-1; sh.hasFill=true; sh.hasStroke=false; svgApplyStyle(sh,tag); std::string d=svgAttr(tag,"d"); if(d.empty())return sh; float cx=0,cy=0,sx=0,sy=0; // current pos and subpath start float lcx2=0,lcy2=0; // last control point (for S/T reflection) char lastCmd=0; size_t i=0; auto nextF=[&]()->float{ while(i=d.size())break; char cmd=d[i]; bool isCmd=isalpha((unsigned char)cmd); if(isCmd){i++;lastCmd=cmd;} else cmd=lastCmd; if(!cmd)break; bool rel=islower((unsigned char)cmd); char C=(char)toupper((unsigned char)cmd); if(C=='M'){ float x=nextF()+(rel?cx:0),y=nextF()+(rel?cy:0); // Track where each subpath starts sh.subpathStarts.push_back((int)sh.verts.size()); sh.verts.push_back({x,y,0,0,0}); cx=x;cy=y; sx=cx;sy=cy; lastCmd=rel?'l':'L'; } else if(C=='L'){ float x=nextF()+(rel?cx:0),y=nextF()+(rel?cy:0); sh.verts.push_back({x,y,0,0,0}); cx=x;cy=y; } else if(C=='H'){ float x=nextF()+(rel?cx:0); sh.verts.push_back({x,cy,0,0,0}); cx=x; } else if(C=='V'){ float y=nextF()+(rel?cy:0); sh.verts.push_back({cx,y,0,0,0}); cy=y; } else if(C=='C'){ float x1=nextF()+(rel?cx:0),y1=nextF()+(rel?cy:0); float x2=nextF()+(rel?cx:0),y2=nextF()+(rel?cy:0); float x=nextF()+(rel?cx:0),y=nextF()+(rel?cy:0); { // Adaptive segments: based on chord length of control polygon float dx1=x1-cx,dy1=y1-cy,dx2=x2-x1,dy2=y2-y1,dx3=x-x2,dy3=y-y2; float clen=std::sqrt(dx1*dx1+dy1*dy1)+std::sqrt(dx2*dx2+dy2*dy2)+std::sqrt(dx3*dx3+dy3*dy3); int seg=std::max(2,(int)(clen/2));if(seg>128)seg=128; for(int s2=1;s2<=seg;s2++){ float t=s2/(float)seg,u=1-t; sh.verts.push_back({u*u*u*cx+3*u*u*t*x1+3*u*t*t*x2+t*t*t*x, u*u*u*cy+3*u*u*t*y1+3*u*t*t*y2+t*t*t*y,0,0,0}); } } lcx2=x2;lcy2=y2; cx=x;cy=y; } else if(C=='S'){ // Smooth cubic: reflect last C's second control point float x1=2*cx-lcx2,y1=2*cy-lcy2; float x2=nextF()+(rel?cx:0),y2=nextF()+(rel?cy:0); float x=nextF()+(rel?cx:0),y=nextF()+(rel?cy:0); for(int s2=1;s2<=20;s2++){ float t=s2/20.f,u=1-t; float bx=u*u*u*cx+3*u*u*t*x1+3*u*t*t*x2+t*t*t*x; float by=u*u*u*cy+3*u*u*t*y1+3*u*t*t*y2+t*t*t*y; sh.verts.push_back({bx,by,0,0,0}); } lcx2=x2;lcy2=y2; cx=x;cy=y; } else if(C=='Q'){ float x1=nextF()+(rel?cx:0),y1=nextF()+(rel?cy:0); float x=nextF()+(rel?cx:0),y=nextF()+(rel?cy:0); for(int s2=1;s2<=20;s2++){ float t=s2/20.f,u=1-t; sh.verts.push_back({u*u*cx+2*u*t*x1+t*t*x,u*u*cy+2*u*t*y1+t*t*y,0,0,0}); } lcx2=x1;lcy2=y1; cx=x;cy=y; } else if(C=='T'){ // Smooth quadratic: reflect last Q's control point float x1=2*cx-lcx2,y1=2*cy-lcy2; float x=nextF()+(rel?cx:0),y=nextF()+(rel?cy:0); for(int s2=1;s2<=20;s2++){ float t=s2/20.f,u=1-t; sh.verts.push_back({u*u*cx+2*u*t*x1+t*t*x,u*u*cy+2*u*t*y1+t*t*y,0,0,0}); } lcx2=x1;lcy2=y1; cx=x;cy=y; } else if(C=='A'){ float rx=nextF(),ry=nextF(); nextF();nextF();nextF(); // x-rot, large-arc, sweep float ex=nextF()+(rel?cx:0),ey=nextF()+(rel?cy:0); // Approximate arc with line to endpoint { // Approximate arc chord length for adaptive segments float adx=ex-cx,ady=ey-cy; float chord=std::sqrt(adx*adx+ady*ady); // rx,ry give the arc radius -- use larger for segment count float r2=std::max(rx,ry); int arcSeg=std::max(4,(int)(chord/2)); if(r2>0){int byRadius=(int)(r2*0.5f);if(byRadius>arcSeg)arcSeg=byRadius;} if(arcSeg>96)arcSeg=96; for(int s2=1;s2<=arcSeg;s2++){ float t=s2/(float)arcSeg; sh.verts.push_back({cx+(ex-cx)*t, cy+(ey-cy)*t,0,0,0}); } } cx=ex;cy=ey; } else if(C=='Z'){ sh.closed=true; sh.verts.push_back({sx,sy,0,0,0}); cx=sx;cy=sy; } else { // Unknown -- skip to next alpha or end while(i tries={path,"data/"+path,"files/"+path}; std::string found; for(auto& t:tries){FILE* f=fopen(t.c_str(),"r");if(f){fclose(f);found=t;break;}} if(found.empty()){std::cerr<<"loadShape: file not found: "<(f)),std::istreambuf_iterator()); PShape* root=new PShape(); root->hasFill=false; // Extract viewBox for scaling float vbW=0,vbH=0; size_t vbp=xml.find("viewBox="); if(vbp!=std::string::npos){ vbp+=9;size_t i2=vbp; svgParseFloat(xml,i2);svgParseFloat(xml,i2); // minX minY vbW=svgParseFloat(xml,i2);vbH=svgParseFloat(xml,i2); } // Parse all shape elements size_t p=0; int maxTags=20000, tagCount=0; std::string currentGroupId; // id from nearest enclosing std::vector<:string> groupIdStack; // stack for nested groups while(p if(lt+3",lt+4); p=(end==std::string::npos)?xml.size():end+3; continue; } // Skip DOCTYPE and CDATA <! ... > (may contain > inside [...]) if(lt+1 accounting for [] nesting size_t i2=lt+2; int brackets=0; while(i2' && brackets<=0){i2++;break;} i2++; } p=i2; continue; } // Find closing > -- skip quoted attribute values size_t gt=lt+1; { bool inQ=false; char qc=0; while(gt=xml.size())break; std::string tag=xml.substr(lt+1,gt-lt-1); p=gt+1; if(tag.empty()||tag[0]=='?')continue; if(tag[0]=='/'){ // Closing tag std::string ctag=tag.substr(1); size_t csp=ctag.find_first_of(" /\t"); if(csp!=std::string::npos) ctag=ctag.substr(0,csp); if(ctag=="g"&&!groupIdStack.empty()){ groupIdStack.pop_back(); currentGroupId=groupIdStack.empty()?"":groupIdStack.back(); } continue; } // Remove newlines/tabs from tag for easier parsing for(size_t ci2=0;ci2float{std::string v=svgAttr(tag,k);if(v.empty())return 0;try{return std::stof(v);}catch(...){return 0;}}; x=a("x");y=a("y");w=a("width");h=a("height");rx=a("rx");ry=a("ry"); child.kind=-1; svgApplyStyle(child,tag); child.verts.push_back({x,y,0,0,0});child.verts.push_back({x+w,y,0,0,0}); child.verts.push_back({x+w,y+h,0,0,0});child.verts.push_back({x,y+h,0,0,0}); child.closed=true; } else if(elem=="circle"||elem=="ellipse"){ float cx2=0,cy2=0,rx=0,ry=0; auto a=[&](const std::string& k)->float{std::string v=svgAttr(tag,k);if(v.empty())return 0;try{return std::stof(v);}catch(...){return 0;}}; cx2=a("cx");cy2=a("cy"); if(elem=="circle"){rx=ry=a("r");}else{rx=a("rx");ry=a("ry");} child.kind=-1; svgApplyStyle(child,tag); int seg=32; for(int s2=0;s2<=seg;s2++){ float t=s2*TWO_PI/seg; child.verts.push_back({cx2+cos(t)*rx,cy2+sin(t)*ry,0,0,0}); } child.closed=true; } else if(elem=="polygon"||elem=="polyline"){ child.kind=-1; svgApplyStyle(child,tag); child.closed=(elem=="polygon"); std::string pts=svgAttr(tag,"points"); size_t pi=0; while(pi=pts.size())break; float x=svgParseFloat(pts,pi),y=svgParseFloat(pts,pi); child.verts.push_back({x,y,0,0,0}); } } else if(elem=="line"){ child.kind=LINES; svgApplyStyle(child,tag); auto a=[&](const std::string& k)->float{std::string v=svgAttr(tag,k);if(v.empty())return 0;try{return std::stof(v);}catch(...){return 0;}}; child.verts.push_back({a("x1"),a("y1"),0,0,0}); child.verts.push_back({a("x2"),a("y2"),0,0,0}); } else if(elem=="defs"||elem=="title"||elem=="desc"){ continue; } else if(elem=="svg"){ continue; } else if(elem=="g"){ // Push group id onto stack so child shapes inherit it std::string gid=svgAttr(tag,"id"); if(gid.empty()) gid=svgAttr(tag,"inkscape:label"); groupIdStack.push_back(gid); currentGroupId=gid.empty()?currentGroupId:gid; continue; } else continue; { std::string sid = svgAttr(tag,"id"); if(sid.empty()) sid = svgAttr(tag,"inkscape:label"); if(sid.empty()) sid = currentGroupId; child.name = sid; // Always add child so getChildCount() matches Java Processing root->children.push_back(child); } // Also handle groups by storing named empty shapes // so getChild can find them even if they hold no direct verts } // Store viewBox dimensions on root shape for reference // Do NOT normalize coordinates -- keep raw SVG space so shape(s,x,y) works // with sketch-supplied offsets. shape(s,x,y,w,h) will scale via glScalef. if(vbW>0&&vbH>0){ root->verts.push_back({vbW,vbH,0,0,0}); // store vbW,vbH as sentinel in root } root->computeBounds(); return root; } // ── OBJ Loader ─────────────────────────────────────────────────────────────── // Per-vertex data for OBJ (stored flat in child PShape) struct ObjVertex { float x,y,z; // position float nx,ny,nz; // normal float u,v; // texcoord }; // Parse MTL file, return map of material name -> texture PImage* static std::unordered_map<:string> objLoadMtl(const std::string& mtlPath){ std::unordered_map<:string> mats; std::ifstream f(mtlPath); if(!f.is_open()) return mats; std::string curMat, line; while(std::getline(f,line)){ if(line.empty()||line[0]=='#') continue; std::istringstream ss(line); std::string tok; ss>>tok; if(tok=="newmtl"){ ss>>curMat; } else if((tok=="map_Kd"||tok=="map_Ka")&&!curMat.empty()&&mats.find(curMat)==mats.end()){ std::string texFile; ss>>texFile; // Try relative to obj dir std::vector<:string> tries={ _s_objDir+texFile, _s_objDir+"data/"+texFile, "data/"+texFile, texFile }; for(auto& t:tries){ #ifdef PROCESSING_HAS_STB_IMAGE int w2,h2,ch; unsigned char* d=stbi_load(t.c_str(),&w2,&h2,&ch,4); if(d){ GLuint tex; glGenTextures(1,&tex); glBindTexture(GL_TEXTURE_2D,tex); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w2,h2,0,GL_RGBA,GL_UNSIGNED_BYTE,d); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT); glBindTexture(GL_TEXTURE_2D,0); stbi_image_free(d); mats[curMat]=tex; break; } #endif } } } return mats; } static PShape* objLoad(const std::string& path){ std::vector<:string> tries={path,"data/"+path,"files/"+path}; std::string found; for(auto& t:tries){FILE* f2=fopen(t.c_str(),"r");if(f2){fclose(f2);found=t;break;}} if(found.empty()){std::cerr<<"loadShape: OBJ not found: "<> vp,vn; std::vector<:array>> vt; std::unordered_map<:string> mats; // material name -> GL texture PShape* root=new PShape(); root->name="__obj__"; // Current group state struct Group { std::string name, matName; GLuint texId=0; std::vector verts; }; std::vector groups; Group* cur=nullptr; auto getGroup=[&]()->Group*{ if(!cur){groups.push_back({});cur=&groups.back();} return cur; }; std::ifstream f(found); std::string line; while(std::getline(f,line)){ // Strip \r if(!line.empty()&&line.back()=='\r') line.pop_back(); if(line.empty()||line[0]=='#') continue; std::istringstream ss(line); std::string tok; ss>>tok; if(tok=="mtllib"){ std::string mtlFile; ss>>mtlFile; std::vector<:string> mtlTries={_s_objDir+mtlFile,"data/"+mtlFile,mtlFile}; for(auto& t:mtlTries){ std::ifstream test(t); if(test.is_open()){mats=objLoadMtl(t);break;} } } else if(tok=="v"){ float x,y,z; ss>>x>>y>>z; vp.push_back({x,y,z}); } else if(tok=="vt"){ float u,v2=0; ss>>u>>v2; vt.push_back({u,v2}); } else if(tok=="vn"){ float x,y,z; ss>>x>>y>>z; vn.push_back({x,y,z}); } else if(tok=="o"||tok=="g"){ groups.push_back({}); cur=&groups.back(); ss>>cur->name; } else if(tok=="usemtl"){ std::string mat; ss>>mat; if(!cur){groups.push_back({});cur=&groups.back();} // Start new group for new material if(!cur->verts.empty()){groups.push_back({});cur=&groups.back();} cur->matName=mat; cur->texId=mats.count(mat)?mats[mat]:0; } else if(tok=="f"){ std::vector<:array>> face; std::string fv; while(ss>>fv){ std::array idx={0,0,0}; std::istringstream fs(fv); std::string part; int fi=0; while(std::getline(fs,part,'/')&&fi<3){ if(!part.empty()) try{idx[fi]=std::stoi(part);}catch(...){} fi++; } face.push_back(idx); } if(face.size()<3) continue; Group* g=getGroup(); for(size_t fi=1;fi+10&&vi<=(int)vp.size()){ov.x=vp[vi-1][0];ov.y=vp[vi-1][1];ov.z=vp[vi-1][2];} else if(vi<0&&(int)vp.size()+vi>=0){auto& p=vp[vp.size()+vi];ov.x=p[0];ov.y=p[1];ov.z=p[2];} if(ti>0&&ti<=(int)vt.size()){ov.u=vt[ti-1][0];ov.v=1.0f-vt[ti-1][1];} // flip V if(ni>0&&ni<=(int)vn.size()){ov.nx=vn[ni-1][0];ov.ny=vn[ni-1][1];ov.nz=vn[ni-1][2];} g->verts.push_back(ov); } } } } // Convert groups to PShape children for(auto& g:groups){ if(g.verts.empty()) continue; PShape child; child.kind=TRIANGLES; child.hasFill=true; child.hasStroke=false; child.fillR=0.8f;child.fillG=0.8f;child.fillB=0.8f;child.fillA=1.0f; child.name=g.matName; // Pack ObjVertex into PShape::Vertex (x,y,z) + subpathStarts for normals + verts.z4,z5 for uv // Store as flat arrays via a different approach: // verts: position (x,y,z), normal (nx via u field, ny via v field... messy) // Better: pack into verts with a 5-float struct matching PShape::Vertex // PShape::Vertex has {x,y,z,u,v} -- repurpose u,v for texcoords // Store normals separately in subpathStarts encoded as 3 floats packed per vertex for(auto& ov:g.verts){ child.verts.push_back({ov.x,ov.y,ov.z,ov.u,ov.v}); } // Store normals as extra data: pack 3 floats per vertex into children[0].verts // Use a sibling child marked as normals store PShape norms; norms.kind=-999; // marker for normal data for(auto& ov:g.verts) norms.verts.push_back({ov.nx,ov.ny,ov.nz,0,0}); child.children.push_back(norms); // Store texture ID in strokeR (reuse float field) // Better: store as a proper field -- use hasTex + texID on PShape // We'll use the existing texId field if it exists, otherwise pack into a child name child.texId=g.texId; root->children.push_back(child); } return root; } PShape* PApplet::loadShape(const std::string& path){ auto ext=[&](const std::string& e)->bool{ return path.size()>=e.size()&&path.substr(path.size()-e.size())==e; }; if(ext(".svg")||ext(".SVG")) return svgLoad(path); if(ext(".obj")||ext(".OBJ")) return objLoad(path); std::cerr<<"loadShape: unsupported format: "<0&&h>0){ // If root has viewBox stored as sentinel, scale to fit w,h float vbW=1,vbH=1; if(!s.verts.empty() && s.children.empty()==false && s.verts[0].z==0){ vbW=s.verts[0].x; vbH=s.verts[0].y; } if(vbW>1) glScalef(w/vbW,h/vbH,1); else glScalef(w,h,1); } for(auto& c:s.children) drawPShape(c,0,0,-1,-1,se); if(!s.verts.empty()){ bool useFill = se ? s.hasFill : doFill; bool useStroke = se ? s.hasStroke : doStroke; int n=(int)s.verts.size(); if(useFill && n>=3){ if(se) glColor4f(s.fillR,s.fillG,s.fillB,s.fillA); else applyFill(); // OBJ triangles: per-vertex normals + texture if(s.kind==TRIANGLES){ const PShape* normStore=nullptr; for(auto& c:s.children) if(c.kind==-999){normStore=&c;break;} bool hasTex=(s.texId!=0); if(hasTex){ glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D,s.texId); glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE); glColor4f(1,1,1,1); } glBegin(GL_TRIANGLES); for(int vi=0;viverts.size()){ glNormal3f(normStore->verts[vi].x,normStore->verts[vi].y,normStore->verts[vi].z); } else if(vi%3==0&&vi+20){nx2/=len;ny2/=len;nz2/=len;} glNormal3f(nx2,ny2,nz2); } if(hasTex) glTexCoord2f(s.verts[vi].u,s.verts[vi].v); glVertex3f(s.verts[vi].x,s.verts[vi].y,s.verts[vi].z); } glEnd(); if(hasTex){glDisable(GL_TEXTURE_2D);glBindTexture(GL_TEXTURE_2D,0);} } else { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); // Use stencil with evenodd rule -- draw each closed subpath separately // so multiple subpaths (e.g. Michigan's two peninsulas) each fill correctly glEnable(GL_STENCIL_TEST); glClear(GL_STENCIL_BUFFER_BIT); glStencilFunc(GL_ALWAYS,0,~0); glStencilOp(GL_KEEP,GL_KEEP,GL_INVERT); glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE); glDisable(GL_CULL_FACE); // Draw each subpath as its own triangle fan into stencil // Uses subpathStarts recorded during path parsing (one entry per M command) { auto drawFan=[&](int from, int to){ if(to-from<3) return; glBegin(GL_TRIANGLE_FAN); for(int vi=from;vi _fontPool; PFont* PApplet::createFont(const std::string& name, float size, bool /*smooth*/) { PFont f(name, size); // Try as file path first, then common system paths // Strip extension from name if already present, to avoid double extensions std::string baseName = name; if (baseName.size() > 4 && (baseName.substr(baseName.size()-4) == ".ttf" || baseName.substr(baseName.size()-4) == ".otf")) { // name already has extension -- use as-is for first attempt } else { baseName = ""; // will build paths with extensions below } std::string nameNoExt = name; if (nameNoExt.size() > 4 && (nameNoExt.substr(nameNoExt.size()-4) == ".ttf" || nameNoExt.substr(nameNoExt.size()-4) == ".otf")) nameNoExt = nameNoExt.substr(0, nameNoExt.size()-4); std::vector<:string> paths = { // Try exact name first (may already have extension) name, // Try adding extensions if not already present nameNoExt + ".ttf", nameNoExt + ".otf", // Sketch data/ folder std::string("data/") + name, std::string("data/") + nameNoExt + ".ttf", std::string("data/") + nameNoExt + ".otf", // Sketch fonts/ folder std::string("fonts/") + name, std::string("fonts/") + nameNoExt + ".ttf", std::string("fonts/") + nameNoExt + ".otf", // Linux system font directories std::string("/usr/share/fonts/truetype/") + nameNoExt + ".ttf", std::string("/usr/share/fonts/opentype/") + nameNoExt + ".otf", std::string("/usr/share/fonts/TTF/") + nameNoExt + ".ttf", std::string("/usr/share/fonts/OTF/") + nameNoExt + ".otf", // Windows system fonts std::string("C:/Windows/Fonts/") + name, std::string("C:/Windows/Fonts/") + nameNoExt + ".ttf", std::string("C:/Windows/Fonts/") + nameNoExt + ".otf", // macOS std::string("/Library/Fonts/") + name, std::string("/System/Library/Fonts/") + name, }; // Also search system font dirs recursively (Linux: fc-list output) #ifndef _WIN32 { FILE* fc = popen(("fc-list : file | grep -i '" + nameNoExt + "' | head -5").c_str(), "r"); if (fc) { char buf[512]; while (fgets(buf, sizeof(buf), fc)) { std::string line(buf); // fc-list format: /path/to/font.ttf: Family:style size_t colon = line.find(':'); if (colon != std::string::npos) { std::string fpath = line.substr(0, colon); // strip whitespace fpath.erase(0, fpath.find_first_not_of(" \t")); fpath.erase(fpath.find_last_not_of(" \t\n\r") + 1); if (!fpath.empty()) paths.push_back(fpath); } } pclose(fc); } } #endif bool found = false; for (auto& p : paths) { if (tryLoadTTF(p, size)) { found = true; break; } } if (!found) { std::cerr << "[font] Could not find font: " << name << "\n"; std::cerr << "[font] Put the .ttf file in your sketch's data/ folder\n"; } _fontPool.push_back(std::move(f)); return &_fontPool.back(); } // textFont -- switch to a previously loaded font void PApplet::textFont(const PFont& font) { currentFont = font; g_textSize = font.size; tryLoadTTF(font.name, font.size); } void PApplet::textFont(const PFont& font, float size) { currentFont = font; currentFont.size = size; g_textSize = size; tryLoadTTF(font.name, size); } // ============================================================================= // TEXTURE // ============================================================================= void PApplet::textureMode(int mode){ textureModeVal=mode; } void PApplet::textureWrap(int mode){ textureWrapVal=mode; GLint wrap=mode==REPEAT?GL_REPEAT:GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,wrap); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,wrap); } void PApplet::texture(PImage& img){ if(img.dirty)img.uploadTexture(); if(img.texID){ glEnable(GL_TEXTURE_2D);glBindTexture(GL_TEXTURE_2D,img.texID); } } // ============================================================================= // FILE / IO HELPERS // ============================================================================= BufferedReader* PApplet::createReader(const std::string& path){ return new BufferedReader(path); } PrintWriter* PApplet::createWriter(const std::string& path){ return new PrintWriter(path); } std::string PApplet::selectInput(const std::string& prompt,const std::string&){ std::string cmd="zenity --file-selection --title=\""+prompt+"\" 2>/dev/null"; FILE* p=popen(cmd.c_str(),"r"); if(!p)return ""; char buf[4096]=""; fgets(buf,sizeof(buf),p); pclose(p); std::string r(buf); if(!r.empty()&&r.back()=='\n')r.pop_back(); return r; } std::string PApplet::selectOutput(const std::string& prompt,const std::string&){ std::string cmd="zenity --file-selection --save --title=\""+prompt+"\" 2>/dev/null"; FILE* p=popen(cmd.c_str(),"r"); if(!p)return ""; char buf[4096]=""; fgets(buf,sizeof(buf),p); pclose(p); std::string r(buf); if(!r.empty()&&r.back()=='\n')r.pop_back(); return r; } std::string PApplet::selectFolder(const std::string& prompt){ std::string cmd="zenity --file-selection --directory --title=\""+prompt+"\" 2>/dev/null"; FILE* p=popen(cmd.c_str(),"r"); if(!p)return ""; char buf[4096]=""; fgets(buf,sizeof(buf),p); pclose(p); std::string r(buf); if(!r.empty()&&r.back()=='\n')r.pop_back(); return r; } PImage* PApplet::requestImage(const std::string& path){ // Create a placeholder: width=-1 means "still loading", width=0 means "failed" // We use a heap PImage and fill it in from a background thread. // The sketch checks img->width != 0 && img->width != -1 to detect completion. PImage* img = new PImage(); img->width = -1; // sentinel: loading in progress img->height = -1; std::thread([img, path]{ // Resolve search paths same as loadImage // Check PROCESSING_SKETCH_PATH env var set by IDE std::string _sketchDir; if (const char* _sp = std::getenv("PROCESSING_SKETCH_PATH")) _sketchDir = std::string(_sp) + "/"; // Also get the directory of the running executable std::string _exeDir; { char _buf[4096] = {}; #ifdef _WIN32 GetModuleFileNameA(nullptr, _buf, sizeof(_buf)); std::string _ep(_buf); size_t _sl = _ep.find_last_of("\\\\"); #else ssize_t _len = readlink("/proc/self/exe", _buf, sizeof(_buf)-1); if (_len > 0) _buf[_len] = 0; std::string _ep(_buf); size_t _sl = _ep.find_last_of("/"); #endif if (_sl != std::string::npos) _exeDir = _ep.substr(0, _sl+1); } std::vector<:string> tries = { path, "data/" + path, "files/" + path, }; std::string found; for (auto& t : tries) { FILE* f = fopen(t.c_str(), "rb"); if (f) { fclose(f); found = t; break; } } if (found.empty()) { // Not found: mark as failed (width=0) img->width = 0; img->height = 0; std::cerr << "requestImage: file not found: " << path << "\n"; return; } #ifdef PROCESSING_HAS_STB_IMAGE int w=0, h=0, ch=0; unsigned char* data = stbi_load(found.c_str(), &w, &h, &ch, 4); if (!data || w<=0 || h<=0) { img->width = 0; img->height = 0; std::cerr << "requestImage: failed to decode: " << path << "\n"; return; } img->pixels.resize((size_t)w * h); for (int i = 0; i < w*h; i++) { unsigned char r=data[i*4+0], g=data[i*4+1], b=data[i*4+2], a=data[i*4+3]; img->pixels[i] = ((unsigned int)a<<24)|((unsigned int)r<<16)| ((unsigned int)g<<8)|(unsigned int)b; } stbi_image_free(data); img->dirty = true; // Write width/height last so the sketch sees a consistent state img->height = h; img->width = w; // width != -1 signals "done" #else img->width = 0; img->height = 0; std::cerr << "requestImage: rebuild with -DPROCESSING_HAS_STB_IMAGE: " << path << "\n"; #endif }).detach(); return img; } // ============================================================================= // PSHADER IMPLEMENTATION // ============================================================================= static std::string readShaderFile(const std::string& path){ std::ifstream f(path); if(!f)return ""; return std::string((std::istreambuf_iterator(f)),std::istreambuf_iterator()); } PShader* PApplet::loadShader(const std::string& fragPath,const std::string& vertPath){ std::string fSrc=readShaderFile(fragPath); std::string vSrc=vertPath.empty()? "#version 120\nvoid main(){gl_Position=ftransform();gl_TexCoord[0]=gl_MultiTexCoord0;gl_FrontColor=gl_Color;}": readShaderFile(vertPath); if(fSrc.empty()){std::cerr<<"loadShader: could not read "<compile(); return s; } void PApplet::shader(PShader& s){ s.bind(); activeShader=&s; } void PApplet::resetShader(){ glUseProgram(0); activeShader=nullptr; } // ============================================================================= // DISPLAY blend() and copy() // ============================================================================= void PApplet::blend(int sx,int sy,int sw,int sh,int dx,int dy,int dw,int dh,int mode){ std::vector src(sw*sh*4); glReadPixels(sx,winHeight-(sy+sh),sw,sh,GL_RGBA,GL_UNSIGNED_BYTE,src.data()); std::vector dst(dw*dh*4); for(int y=0;y buf(w*h*4); glReadPixels(x,(PApplet::g_papplet?PApplet::g_papplet->winHeight:0)-(y+h),w,h,GL_RGBA,GL_UNSIGNED_BYTE,buf.data()); for(int iy=0;iy PApplet::loadStrings(const std::string& path) { std::vector<:string> lines; std::string sketchDir; if (const char* sp = std::getenv("PROCESSING_SKETCH_PATH")) sketchDir = std::string(sp) + "/"; std::ifstream f(path); if (!f) f.open(sketchDir + path); if (!f) f.open(sketchDir + "data/" + path); std::string line; while (std::getline(f, line)) lines.push_back(line); return lines; } bool PApplet::saveStrings(const std::string& path, const std::vector<:string>& lines) { std::ofstream f(path); if (!f) return false; for (auto& l : lines) f << l << "\n"; return true; } std::vector PApplet::loadBytes(const std::string& path) { std::ifstream f(path, std::ios::binary); return std::vector((std::istreambuf_iterator(f)), std::istreambuf_iterator()); } bool PApplet::saveBytes(const std::string& path, const std::vector& data) { std::ofstream f(path, std::ios::binary); if (!f) return false; f.write(reinterpret_cast(data.data()), data.size()); return true; } std::vector<:string> PApplet::split(const std::string& s, char delim) { std::vector<:string> r; std::string t; for (char c : s) { if (c==delim){r.push_back(t);t.clear();}else t+=c; } r.push_back(t); return r; } std::vector<:string> PApplet::splitTokens(const std::string& s, const std::string& delims) { std::vector<:string> r; std::string t; for (char c : s) { if (delims.find(c)!=std::string::npos){if(!t.empty()){r.push_back(t);t.clear();}}else t+=c; } if (!t.empty()) r.push_back(t); return r; } std::string PApplet::join(const std::vector<:string>& v, const std::string& sep) { std::string r; for (size_t i=0;i PApplet::nf(const std::vector& nums) { return Processing::nf(nums); } std::vector<:string> PApplet::nf(const std::vector& nums, int digits) { return Processing::nf(nums, digits); } std::vector<:string> PApplet::nf(const std::vector& nums, int left, int right) { return Processing::nf(nums, left, right); } std::string PApplet::nfc(int v) { return Processing::nfc(v); } std::string PApplet::nfc(float v, int right) { return Processing::nfc(v, right); } std::vector<:string> PApplet::nfc(const std::vector& nums) { return Processing::nfc(nums); } std::vector<:string> PApplet::nfc(const std::vector& nums, int right) { return Processing::nfc(nums, right); } std::string PApplet::nfp(int v) { return Processing::nfp(v); } std::string PApplet::nfp(int v, int digits) { return Processing::nfp(v, digits); } std::string PApplet::nfp(float v, int left, int right) { return Processing::nfp(v, left, right); } std::vector<:string> PApplet::nfp(const std::vector& nums) { return Processing::nfp(nums); } std::vector<:string> PApplet::nfp(const std::vector& nums, int digits) { return Processing::nfp(nums, digits); } std::vector<:string> PApplet::nfp(const std::vector& nums, int left, int right) { return Processing::nfp(nums, left, right); } std::string PApplet::nfs(int v) { return Processing::nfs(v); } std::string PApplet::nfs(int v, int digits) { return Processing::nfs(v, digits); } std::string PApplet::nfs(float v, int left, int right) { return Processing::nfs(v, left, right); } std::vector<:string> PApplet::nfs(const std::vector& nums) { return Processing::nfs(nums); } std::vector<:string> PApplet::nfs(const std::vector& nums, int digits) { return Processing::nfs(nums, digits); } std::vector<:string> PApplet::nfs(const std::vector& nums, int left, int right) { return Processing::nfs(nums, left, right); } std::string PApplet::hex(int v) { std::ostringstream ss; ss<<:uppercase return ss.str std::string papplet::binary v r for i="31;i">=0;i--) r+=((v>>i)&1)?'1':'0'; return r; } } // namespace Processing