Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions src/webgl/ShapeBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes this is one of the currently kinda awkward bits of 2.x now that we've split core functionality into addons: there is not always a global p5. The way we're currently dealing with this is with a sort of "dependency injection" pattern of sorts, where we make a function on the current class, like in this case maybe friendlyErrorsDisabled() { return false; }, and then in an addon file inside of function myAddon(p5, fn, lifecycles) { ... }, we override that function with a real implementation. Take a look in this PR at how we've given p5.Image access to p5.friendlyError: https://github.com/processing/p5.js/pull/8676/changes#diff-fdf1c89a0511ed981f59e9539de1ff707da1e75c7b21f35184406cacb5781672

Let's maybe try to follow a similar pattern here where we override ShapeBuilder.friendlyErrorsDisabled (or whatever we choose to call it) from within the addon function that adds the WebGL renderer to p5?

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;
Expand Down
92 changes: 92 additions & 0 deletions test/unit/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading