diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 1fde0670bc..21d246eca0 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -221,6 +221,8 @@ export class Renderer3D extends Renderer { // Used by beginShape/endShape functions to construct a p5.Geometry this.shapeBuilder = new ShapeBuilder(this); + this._largeTessellationAcknowledged = false; + this.geometryBufferCache = new GeometryBufferCache(this); this.curStrokeCap = constants.ROUND; diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 124fa62bfa..6a2c723f78 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -148,6 +148,29 @@ export class ShapeBuilder { } if (this.shapeMode === constants.PATH) { + const vertexCount = this.geometry.vertices.length; + const MAX_SAFE_TESSELLATION_VERTICES = 50000; + + if (vertexCount > MAX_SAFE_TESSELLATION_VERTICES) { + const p5Class = this.renderer._pInst.constructor; + if ( + !p5Class.disableFriendlyErrors && + !this.renderer._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.renderer._largeTessellationAcknowledged = true; + } + } + this.isProcessingVertices = true; this._tesselateShape(); this.isProcessingVertices = false; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 4d3eaaf7aa..c0e2e94a49 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2032,6 +2032,98 @@ suite('p5.RendererGL', function() { [-10, 0, 10] ); }); + + suite('large tessellation guard', function() { + test('prompts user before tessellating >50k vertices', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + const tessSpy = vi.spyOn( + renderer.shapeBuilder, + '_tesselateShape' + ).mockImplementation(() => {}); + + myp5.beginShape(); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).toHaveBeenCalled(); + expect(confirmSpy.mock.calls[0][0]).toContain('60000'); + expect(tessSpy).not.toHaveBeenCalled(); + + confirmSpy.mockRestore(); + tessSpy.mockRestore(); + }); + + test('only prompts once when user approves large tessellation', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); + const tessSpy = vi.spyOn( + renderer.shapeBuilder, + '_tesselateShape' + ).mockImplementation(() => {}); + + myp5.beginShape(); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + expect(renderer._largeTessellationAcknowledged).toBe(true); + + myp5.beginShape(); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + + confirmSpy.mockRestore(); + tessSpy.mockRestore(); + }); + + test('skips prompt when p5.disableFriendlyErrors is true', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + const tessSpy = vi.spyOn( + renderer.shapeBuilder, + '_tesselateShape' + ).mockImplementation(() => {}); + p5.disableFriendlyErrors = true; + + myp5.beginShape(); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(tessSpy).toHaveBeenCalled(); + + p5.disableFriendlyErrors = false; + confirmSpy.mockRestore(); + tessSpy.mockRestore(); + }); + + test('works normally for <50k vertices', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + + myp5.beginShape(); + myp5.vertex(-10, -10, 0); + myp5.vertex(10, -10, 0); + myp5.vertex(10, 10, 0); + myp5.vertex(-10, 10, 0); + myp5.endShape(myp5.CLOSE); + + expect(confirmSpy).not.toHaveBeenCalled(); + + confirmSpy.mockRestore(); + }); + }); }); suite('color interpolation', function() {