diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 31ce48f630..b222670ea5 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -313,6 +313,25 @@ 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 (!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) { + return; + } + this._largeTessellationAcknowledged = true; + } + } + this._tesselateShape(); } }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 27583e036a..24072bdd78 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -637,6 +637,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } }; + 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..f1e9a57ed2 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1686,6 +1686,119 @@ suite('p5.RendererGL', function() { done(); }); + test('TESS mode prompts user before tessellating >50k vertices', function(done) { + var renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + 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); + var confirmStub = sinon.stub(window, 'confirm').returns(true); + var tessellateStub = sinon.stub(renderer, '_tesselateShape'); + + 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' + ); + + 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(); + tessellateStub.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); + var tessellateStub = sinon.stub(renderer, '_tesselateShape'); + 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' + ); + assert.isTrue( + tessellateStub.called, + 'tessellation should proceed without prompt when p5.disableFriendlyErrors is true' + ); + + p5.disableFriendlyErrors = false; + confirmStub.restore(); + tessellateStub.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);