From 5155fbcc5229b69c8bf5edd4631937ad87412377 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Fri, 31 Oct 2025 23:16:37 +0530 Subject: [PATCH 1/4] fix(webgl): normalize nearly identical vertices before tessellation --- src/webgl/ShapeBuilder.js | 26 ++++++++++++++++++++++++++ test/unit/visual/cases/webgl.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 4b0099db50..30a5c2ce2f 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -313,6 +313,32 @@ export class ShapeBuilder { } } + // Normalize nearly identical consecutive vertices to prevent tessellation artifacts + // This addresses numerical precision issues in libtess when consecutive vertices + // have coordinates that are almost (but not exactly) equal (e.g., differing by ~1e-8) + const epsilon = 1e-6; + for (const contour of contours) { + const stride = this.tessyVertexSize; + for (let i = stride; i < contour.length; i += stride) { + const prevX = contour[i - stride]; + const prevY = contour[i - stride + 1]; + const prevZ = contour[i - stride + 2]; + const currX = contour[i]; + const currY = contour[i + 1]; + const currZ = contour[i + 2]; + + if (Math.abs(currX - prevX) < epsilon) { + contour[i] = prevX; + } + if (Math.abs(currY - prevY) < epsilon) { + contour[i + 1] = prevY; + } + if (Math.abs(currZ - prevZ) < epsilon) { + contour[i + 2] = prevZ; + } + } + } + const polyTriangles = this._triangulate(contours); // If there were no valid faces, we still want to use the original vertices diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 670c5eda00..c35f4af420 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -693,4 +693,35 @@ visualSuite('WebGL', function() { screenshot(); }); }); + + visualSuite('Tessellation', function() { + visualTest('Handles nearly identical consecutive vertices', function(p5, screenshot) { + p5.createCanvas(100, 100, p5.WEBGL); + p5.pixelDensity(1); + p5.background(255); + p5.fill(0); + p5.noStroke(); + + // Contours with nearly identical consecutive vertices (as can occur with textToContours) + // Outer contour + p5.beginShape(); + p5.vertex(-30, -30, 0); + p5.vertex(30, -30, 0); + p5.vertex(30, 30, 0); + p5.vertex(-30, 30, 0); + + // Inner contour (hole) with nearly identical vertices + p5.beginContour(); + p5.vertex(-10, -10, 0); + p5.vertex(-10, 10, 0); + // This vertex has x coordinate almost equal to previous (10.00000001 vs 10) + p5.vertex(10.00000001, 10, 0); + p5.vertex(10, -10, 0); + p5.endContour(); + + p5.endShape(p5.CLOSE); + + screenshot(); + }); + }); }); From c4a910ce8e082ef80cdf3b76183c7a4c8fa37a25 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Mon, 24 Nov 2025 12:46:04 +0530 Subject: [PATCH 2/4] fix(webgl): normalize nearly identical vertices before tessellation - Remove Z-axis normalization (only X/Y needed based on testing) - Update visual test to use textToContours() example from issue #8186 - Test now reproduces the actual bug that gets fixed Resolves #8186 --- src/webgl/ShapeBuilder.js | 5 ---- test/unit/visual/cases/webgl.js | 41 ++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 30a5c2ce2f..124fa62bfa 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -322,10 +322,8 @@ export class ShapeBuilder { for (let i = stride; i < contour.length; i += stride) { const prevX = contour[i - stride]; const prevY = contour[i - stride + 1]; - const prevZ = contour[i - stride + 2]; const currX = contour[i]; const currY = contour[i + 1]; - const currZ = contour[i + 2]; if (Math.abs(currX - prevX) < epsilon) { contour[i] = prevX; @@ -333,9 +331,6 @@ export class ShapeBuilder { if (Math.abs(currY - prevY) < epsilon) { contour[i + 1] = prevY; } - if (Math.abs(currZ - prevZ) < epsilon) { - contour[i + 2] = prevZ; - } } } diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 22a3209c6f..c9b0adec0a 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1244,29 +1244,32 @@ visualSuite('WebGL', function() { }); visualSuite('Tessellation', function() { - visualTest('Handles nearly identical consecutive vertices', function(p5, screenshot) { - p5.createCanvas(100, 100, p5.WEBGL); - p5.pixelDensity(1); + visualTest('Handles nearly identical consecutive vertices from textToContours', async function(p5, screenshot) { + p5.createCanvas(200, 200, p5.WEBGL); p5.background(255); p5.fill(0); p5.noStroke(); - // Outer contour - p5.beginShape(); - p5.vertex(-30, -30, 0); - p5.vertex(30, -30, 0); - p5.vertex(30, 30, 0); - p5.vertex(-30, 30, 0); - - // Inner contour (hole) - p5.beginContour(); - p5.vertex(-10, -10, 0); - p5.vertex(-10, 10, 0); - p5.vertex(10.00000001, 10, 0); - p5.vertex(10, -10, 0); - p5.endContour(); - - p5.endShape(p5.CLOSE); + const font = await p5.loadFont('/unit/assets/Inconsolata-Bold.ttf'); + const contours = font.textToContours('p', 0, 0, 60); + + if (contours && contours.length > 0) { + p5.translate(-p5.width / 4, -p5.height / 4); + p5.beginShape(); + for (let contourIdx = 0; contourIdx < contours.length; contourIdx++) { + const contour = contours[contourIdx]; + if (contourIdx > 0) { + p5.beginContour(); + } + for (let i = 0; i < contour.length; i++) { + p5.vertex(contour[i].x, contour[i].y, 0); + } + if (contourIdx > 0) { + p5.endContour(); + } + } + p5.endShape(p5.CLOSE); + } screenshot(); }); From d9083ca1214291ed7244d1d9b460d914406364fd Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Sun, 7 Dec 2025 10:48:39 +0530 Subject: [PATCH 3/4] test: use exact contour data that reproduces tessellation bug Replace textToContours test with hardcoded contour data from #8186 This test actually shows the tessellation artifacts before the fix --- test/unit/visual/cases/webgl.js | 89 ++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index c9b0adec0a..375a3ebfaf 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1244,32 +1244,73 @@ visualSuite('WebGL', function() { }); visualSuite('Tessellation', function() { - visualTest('Handles nearly identical consecutive vertices from textToContours', async function(p5, screenshot) { - p5.createCanvas(200, 200, p5.WEBGL); - p5.background(255); - p5.fill(0); - p5.noStroke(); - - const font = await p5.loadFont('/unit/assets/Inconsolata-Bold.ttf'); - const contours = font.textToContours('p', 0, 0, 60); - - if (contours && contours.length > 0) { - p5.translate(-p5.width / 4, -p5.height / 4); - p5.beginShape(); - for (let contourIdx = 0; contourIdx < contours.length; contourIdx++) { - const contour = contours[contourIdx]; - if (contourIdx > 0) { - p5.beginContour(); - } - for (let i = 0; i < contour.length; i++) { - p5.vertex(contour[i].x, contour[i].y, 0); - } - if (contourIdx > 0) { - p5.endContour(); - } + visualTest('Handles nearly identical consecutive vertices', function(p5, screenshot) { + p5.createCanvas(400, 400, p5.WEBGL); + + const contours = [ + [ + [-3.8642425537109375, -6.120738636363637, 0], + [3.2025188099254267, -6.120738636363637, 0], + [3.2025188099254267, -4.345170454545455, 0], + [-3.8642425537109375, -4.345170454545455, 0], + [-3.8642425537109375, -6.120738636363637, 0] + ], + [ + [-1.8045834628018462, 4.177556818181818, 0], + [-1.8045834628018462, -9.387784090909093, 0], + [0.29058699174360836, -9.387784090909093, 0], + [0.2905869917436083, 3.609374411367136, 0], + [0.31044303036623855, 4.068235883781435, 0], + [0.38522861430307975, 4.522728865422799, 0], + [0.548044378107245, 4.941051136363637, 0], + [0.8364672032828204, 5.2932224887960775, 0], + [1.2227602871981542, 5.526988636363637, 0], + [1.6572258237923885, 5.634502949876295, 0], + [2.101666537198154, 5.669034090909091, 0], + [2.6695604948237173, 5.633568761673102, 0], + [3.0249619917436084, 5.5625, 0], + [3.4510983553799726, 7.4446022727272725, 0], + [2.8568950819856695, 7.613138889205699, 0], + [2.3751340936529037, 7.676962586830456, 0], + [1.8892600236717598, 7.693181792704519, 0], + [1.2922705720786674, 7.649533731133848, 0], + [0.7080836288276859, 7.519788939617751, 0], + [0.14854153719815422, 7.311434659090909, 0], + [-0.38643934048179873, 7.00959666478984, 0], + [-0.858113258144025, 6.61653855366859, 0], + [-1.25415732643821, 6.1484375, 0], + [-1.5108595282965422, 5.697682732328092, 0], + [-1.6824918355513252, 5.207533878495854, 0], + [-1.7762971052870198, 4.695933154267308, 0], + [-1.8045834628018462, 4.177556818181818, 0] + ] + ]; + + p5.background('red'); + p5.push(); + p5.stroke(0); + p5.fill('#EEE'); + p5.scale(15); + p5.beginShape(); + for (const contour of contours) { + p5.beginContour(); + for (const v of contour) { + p5.vertex(...v); } - p5.endShape(p5.CLOSE); + p5.endContour(); } + p5.endShape(); + + p5.stroke(0, 255, 0); + p5.strokeWeight(5); + p5.beginShape(p5.POINTS); + for (const contour of contours) { + for (const v of contour) { + p5.vertex(...v); + } + } + p5.endShape(); + p5.pop(); screenshot(); }); From 3ea1595d9f4894ed9d6321293836a7564d367018 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Wed, 28 Jan 2026 11:00:58 +0530 Subject: [PATCH 4/4] test: add visual test screenshot and metadata --- .../000.png | Bin 0 -> 6725 bytes .../metadata.json | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGL/Tessellation/Handles nearly identical consecutive vertices/000.png create mode 100644 test/unit/visual/screenshots/WebGL/Tessellation/Handles nearly identical consecutive vertices/metadata.json diff --git a/test/unit/visual/screenshots/WebGL/Tessellation/Handles nearly identical consecutive vertices/000.png b/test/unit/visual/screenshots/WebGL/Tessellation/Handles nearly identical consecutive vertices/000.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc83482af24d2cfea2b048e6ec033a683b251f6 GIT binary patch literal 6725 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