diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 31ce48f630..6c443c87df 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -313,6 +313,29 @@ p5.RendererGL.prototype._processVertices = function(mode) { this.immediateMode.shapeMode !== constants.LINES; if (shouldTess) { + const vertexCount = this.immediateMode.geometry.vertices.length; + const MAX_SAFE_TESSELLATION_VERTICES = 50000; + + if (vertexCount > MAX_SAFE_TESSELLATION_VERTICES) { + // If FES is disabled (or minified build), just run tessellation as-is. + // Otherwise, prompt the user once to decide whether to continue. + if (!p5.disableFriendlyErrors && !this._largeTessellationAcknowledged) { + const proceed = window.confirm( + '🌸 p5.js says:\n\n' + + `This shape has ${vertexCount} vertices. Tessellating shapes with this ` + + 'many vertices can be very slow and may cause your browser to become ' + + 'unresponsive.\n\n' + + 'Do you want to continue tessellating this shape?' + ); + if (!proceed) { + // User cancelled — draw nothing for this shape. + return; + } + // User approved — skip this prompt for the rest of the session. + this._largeTessellationAcknowledged = true; + } + } + this._tesselateShape(); } }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 27583e036a..ae6cdfb885 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -637,6 +637,11 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } }; + // Flag set to true once the user approves tessellating a large shape via + // the confirm() prompt. Once set, we skip the prompt for the rest of the + // session so the user is only interrupted once. + this._largeTessellationAcknowledged = false; + this.curStrokeWeight = 1; this.pointSize = this.curStrokeWeight; this.curStrokeCap = constants.ROUND; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 3c7e3df1a2..47d340d421 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1686,6 +1686,115 @@ suite('p5.RendererGL', function() { done(); }); + test('TESS mode prompts user before tessellating >50k vertices', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + // Stub confirm() so the user "cancels" — shape should draw nothing + var confirmStub = sinon.stub(window, 'confirm').returns(false); + + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.isTrue( + confirmStub.called, + 'window.confirm should be called when vertex count exceeds threshold' + ); + assert.isTrue( + confirmStub.args[0][0].includes('60000'), + 'confirm message should include the actual vertex count' + ); + // Shape mode must NOT be changed to TRIANGLE_FAN — draw nothing on cancel + assert.notEqual( + renderer.immediateMode.shapeMode, + myp5.TRIANGLE_FAN, + 'Shape mode should not fall back to TRIANGLE_FAN when user cancels' + ); + + confirmStub.restore(); + done(); + }); + + test('TESS mode only prompts once when user approves large tessellation', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + // User approves on the first prompt + var confirmStub = sinon.stub(window, 'confirm').returns(true); + + // First large shape — should prompt + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.equal(confirmStub.callCount, 1, 'confirm should be called once on first large shape'); + assert.isTrue( + renderer._largeTessellationAcknowledged, + '_largeTessellationAcknowledged should be set after user approves' + ); + + // Second large shape — should NOT prompt again + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.equal(confirmStub.callCount, 1, 'confirm should not be called again after acknowledgement'); + + confirmStub.restore(); + done(); + }); + + test('TESS mode skips prompt when p5.disableFriendlyErrors is true', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + var confirmStub = sinon.stub(window, 'confirm').returns(false); + p5.disableFriendlyErrors = true; + + renderer.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + renderer.vertex(i % 100, Math.floor(i / 100), 0); + } + renderer.endShape(); + + assert.isFalse( + confirmStub.called, + 'window.confirm should not be called when p5.disableFriendlyErrors is true' + ); + + p5.disableFriendlyErrors = false; + confirmStub.restore(); + done(); + }); + + test('TESS mode works normally for <50k vertices', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + var confirmStub = sinon.stub(window, 'confirm').returns(false); + + // use a simple shape that tessellates quickly + renderer.beginShape(myp5.TESS); + renderer.vertex(-10, -10, 0); + renderer.vertex(10, -10, 0); + renderer.vertex(10, 10, 0); + renderer.vertex(-10, 10, 0); + renderer.endShape(myp5.CLOSE); + + assert.isFalse( + confirmStub.called, + 'window.confirm should not be called for shapes with fewer than 50k vertices' + ); + + assert.equal( + renderer.immediateMode.shapeMode, + myp5.TRIANGLES, + 'Shape mode should be TRIANGLES after normal tessellation' + ); + + confirmStub.restore(); + done(); + }); + test('TESS does not affect stroke colors', function(done) { var renderer = myp5.createCanvas(10, 10, myp5.WEBGL);