From 91904917f4992269299cbed592fa2fed6a7f3e94 Mon Sep 17 00:00:00 2001 From: nityam Date: Fri, 17 Apr 2026 15:26:59 +0530 Subject: [PATCH 1/3] prompt before tessellating very large shapes --- src/core/p5.Renderer3D.js | 2 + src/webgl/ShapeBuilder.js | 23 ++++++++ test/unit/webgl/p5.RendererGL.js | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) 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..22ede1f326 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(myp5.TESS); + 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(myp5.TESS); + 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(myp5.TESS); + 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(myp5.TESS); + 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.TESS); + 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() { From 746e46c2e004ee1c306a404a9ce220760f9287bd Mon Sep 17 00:00:00 2001 From: nityam Date: Fri, 17 Apr 2026 15:47:54 +0530 Subject: [PATCH 2/3] use beginShape without TESS arg for dev-2.0 path mode --- test/unit/webgl/p5.RendererGL.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 22ede1f326..c0e2e94a49 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2042,7 +2042,7 @@ suite('p5.RendererGL', function() { '_tesselateShape' ).mockImplementation(() => {}); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2064,7 +2064,7 @@ suite('p5.RendererGL', function() { '_tesselateShape' ).mockImplementation(() => {}); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2073,7 +2073,7 @@ suite('p5.RendererGL', function() { expect(confirmSpy).toHaveBeenCalledTimes(1); expect(renderer._largeTessellationAcknowledged).toBe(true); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2094,7 +2094,7 @@ suite('p5.RendererGL', function() { ).mockImplementation(() => {}); p5.disableFriendlyErrors = true; - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2112,7 +2112,7 @@ suite('p5.RendererGL', function() { const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); myp5.vertex(-10, -10, 0); myp5.vertex(10, -10, 0); myp5.vertex(10, 10, 0); From 41a0e50d6b3316bd80b8a005a56a3c5303bf76bf Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 20:21:38 +0530 Subject: [PATCH 3/3] use dependency injection pattern for friendly errors check --- src/core/p5.Renderer3D.js | 4 ++++ src/webgl/ShapeBuilder.js | 36 +++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 21d246eca0..e8eb0b3179 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1997,6 +1997,10 @@ const webGPUAddonMessage = 'Add the WebGPU add-on to your project and pass WEBGP function renderer3D(p5, fn) { p5.Renderer3D = Renderer3D; + ShapeBuilder.prototype.friendlyErrorsDisabled = function() { + return Boolean(p5.disableFriendlyErrors); + }; + /** * Creates a `p5.StorageBuffer`, which is * a block of data that shaders can read from, and compute shaders diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 6a2c723f78..9fbcf5955c 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -45,6 +45,10 @@ export class ShapeBuilder { this.bufferStrides = { ...INITIAL_BUFFER_STRIDES }; } + friendlyErrorsDisabled() { + return false; + } + constructFromContours(shape, contours) { if (this._useUserVertexProperties){ this._resetUserVertexProperties(); @@ -151,24 +155,22 @@ export class ShapeBuilder { 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; + if ( + vertexCount > MAX_SAFE_TESSELLATION_VERTICES && + !this.friendlyErrorsDisabled() && + !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;