diff --git a/lib/empty-example/index.html b/lib/empty-example/index.html
index 56c88a89b8..54b1bfdfe2 100644
--- a/lib/empty-example/index.html
+++ b/lib/empty-example/index.html
@@ -12,7 +12,7 @@
background-color: #1b1b1b;
}
-
+
diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js
index c614f47b93..3b725f9da9 100644
--- a/lib/empty-example/sketch.js
+++ b/lib/empty-example/sketch.js
@@ -1,7 +1,7 @@
function setup() {
- // put setup code here
-}
-
-function draw() {
- // put drawing code here
-}
+ // put setup code here
+ }
+
+ function draw() {
+ // put drawing code here
+ }
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index edccbe8993..835c5da0fa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,11 +13,11 @@
"acorn": "^8.12.1",
"acorn-walk": "^8.3.4",
"colorjs.io": "^0.5.2",
+ "earcut": "^3.0.1",
"file-saver": "^1.3.8",
"gifenc": "^1.0.3",
"i18next": "^19.0.2",
"i18next-browser-languagedetector": "^4.0.1",
- "libtess": "^1.2.2",
"omggif": "^1.0.10",
"pako": "^2.1.0",
"zod": "^3.23.8"
@@ -4123,6 +4123,12 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/earcut": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
+ "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
+ "license": "ISC"
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -6367,12 +6373,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/libtess": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/libtess/-/libtess-1.2.2.tgz",
- "integrity": "sha512-Nps8HPeVVcsmJxUvFLKVJcCgcz+1ajPTXDVAVPs6+giOQP4AHV31uZFFkh+CKow/bkB7GbZWKmwmit7myaqDSw==",
- "license": "SGI-B-2.0"
- },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
diff --git a/package.json b/package.json
index e34a8ef5c7..012ae3db8f 100644
--- a/package.json
+++ b/package.json
@@ -28,11 +28,11 @@
"acorn": "^8.12.1",
"acorn-walk": "^8.3.4",
"colorjs.io": "^0.5.2",
+ "earcut": "^3.0.1",
"file-saver": "^1.3.8",
"gifenc": "^1.0.3",
"i18next": "^19.0.2",
"i18next-browser-languagedetector": "^4.0.1",
- "libtess": "^1.2.2",
"omggif": "^1.0.10",
"pako": "^2.1.0",
"zod": "^3.23.8"
diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js
index ffd984b829..68bacad8ad 100644
--- a/src/type/p5.Font.js
+++ b/src/type/p5.Font.js
@@ -126,7 +126,7 @@ function font(p5, fn) {
for (const { x, y } of contour) {
this._pInst.vertex(x, y);
}
- this._pInst.endContour(this._pInst.CLOSE);
+ this._pInst.endContour();
}
this._pInst.endShape();
} else {
@@ -138,7 +138,7 @@ function font(p5, fn) {
for (const { x, y } of contour) {
this._pInst.vertex(x, y, side * extrude * 0.5);
}
- this._pInst.endContour(this._pInst.CLOSE);
+ this._pInst.endContour();
}
this._pInst.endShape();
this._pInst.beginShape();
@@ -792,4 +792,4 @@ export default font;
if (typeof p5 !== 'undefined') {
font(p5, p5.prototype);
-}
+}
\ No newline at end of file
diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js
index 41535345e7..bcb2b3adb7 100644
--- a/src/webgl/ShapeBuilder.js
+++ b/src/webgl/ShapeBuilder.js
@@ -1,6 +1,6 @@
import * as constants from '../core/constants';
import { Geometry } from './p5.Geometry';
-import libtess from 'libtess'; // Fixed with exporting module from libtess
+import earcut, {flatten, deviation} from 'earcut';
import { Vector } from '../math/p5.Vector';
import { RenderBuffer } from './p5.RenderBuffer';
@@ -22,7 +22,7 @@ export class ShapeBuilder {
this.renderer = renderer;
this.shapeMode = constants.PATH;
this.geometry = new Geometry(undefined, undefined, undefined, this.renderer);
- this.geometry.gid = '__IMMEDIATE_MODE_GEOMETRY__';
+ this.geometry.gid = '_IMMEDIATE_MODE_GEOMETRY_';
this.contourIndices = [];
this._useUserVertexProperties = undefined;
@@ -33,9 +33,6 @@ export class ShapeBuilder {
// Used to distinguish between user calls to vertex() and internal calls
this.isProcessingVertices = false;
-
- // Used for converting shape outlines into triangles for rendering
- this._tessy = this._initTessy();
this.tessyVertexSize = INITIAL_VERTEX_SIZE;
this.bufferStrides = { ...INITIAL_BUFFER_STRIDES };
}
@@ -259,19 +256,14 @@ export class ShapeBuilder {
* @private
*/
_tesselateShape() {
- // TODO: handle non-PATH shape modes that have contours
this.shapeMode = constants.TRIANGLES;
- // const contours = [[]];
const contours = [];
for (let i = 0; i < this.geometry.vertices.length; i++) {
- if (
- this.contourIndices.length > 0 &&
- this.contourIndices[0] === i
- ) {
+ if (this.contourIndices.length > 0 && this.contourIndices[0] === i) {
this.contourIndices.shift();
contours.push([]);
}
- contours[contours.length-1].push(
+ contours[contours.length - 1].push(
this.geometry.vertices[i].x,
this.geometry.vertices[i].y,
this.geometry.vertices[i].z,
@@ -377,108 +369,94 @@ export class ShapeBuilder {
this.geometry.vertexColors = colors;
}
- _initTessy() {
- // function called for each vertex of tesselator output
- function vertexCallback(data, polyVertArray) {
- for (const element of data) {
- polyVertArray.push(element);
+ _triangulate(contours) {
+ const allTriangleVerts = [];
+
+ // 1. Classify contours as outer shapes or holes using winding order
+ const classifiedContours = contours.map(contour => {
+ const polygon = [];
+ for (let j = 0; j < contour.length; j += this.tessyVertexSize) {
+ polygon.push([contour[j], contour[j + 1]]);
}
- }
-
- function begincallback(type) {
- if (type !== libtess.primitiveType.GL_TRIANGLES) {
- console.log(`expected TRIANGLES but got type: ${type}`);
+ return {
+ isHole: this._isClockwise(polygon),
+ polygon,
+ vertexData: contour
+ };
+ });
+
+ // 2. Group holes with their parent outer contours
+ const contourGroups = [];
+ for (const c of classifiedContours) {
+ if (!c.isHole) {
+ // Outer contour - start new group
+ contourGroups.push({
+ outer: c,
+ holes: []
+ });
+ } else {
+ // Find parent outer contour that contains this hole
+ const parent = contourGroups.find(g =>
+ this._contains(g.outer.polygon, c.polygon[0])
+ );
+ if (parent) parent.holes.push(c);
}
}
-
- function errorcallback(errno) {
- console.log('error callback');
- console.log(`error number: ${errno}`);
- }
-
- // callback for when segments intersect and must be split
- const combinecallback = (coords, data, weight) => {
- const result = new Array(this.tessyVertexSize).fill(0);
- for (let i = 0; i < weight.length; i++) {
- for (let j = 0; j < result.length; j++) {
- if (weight[i] === 0 || !data[i]) continue;
- result[j] += data[i][j] * weight[i];
- }
+
+ // 3. Triangulate each group separately
+ for (const group of contourGroups) {
+ const { outer, holes } = group;
+ const polygons = [outer.polygon, ...holes.map(h => h.polygon)];
+
+ // Flatten and triangulate
+ const { vertices: verts2D, holes: earcutHoles, dimensions } = flatten(polygons);
+ const indices = earcut(verts2D, earcutHoles, dimensions);
+
+ // Get deviation for this group
+ const dev = deviation(verts2D, earcutHoles, dimensions, indices);
+ console.log('Group deviation:', dev);
+
+ // Collect vertices
+ const vertexData = [];
+ for (let j = 0; j < outer.vertexData.length; j += this.tessyVertexSize) {
+ vertexData.push(outer.vertexData.slice(j, j + this.tessyVertexSize));
}
- return result;
- };
-
- function edgeCallback(flag) {
- // don't really care about the flag, but need no-strip/no-fan behavior
- }
-
- const tessy = new libtess.GluTesselator();
- tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback);
- tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback);
- tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback);
- tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback);
- tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback);
- tessy.gluTessProperty(
- libtess.gluEnum.GLU_TESS_WINDING_RULE,
- libtess.windingRule.GLU_TESS_WINDING_NONZERO
- );
-
- return tessy;
- }
-
- /**
- * Runs vertices through libtess to convert them into triangles
- * @private
- */
- _triangulate(contours) {
- // libtess will take 3d verts and flatten to a plane for tesselation.
- // libtess is capable of calculating a plane to tesselate on, but
- // if all of the vertices have the same z values, we'll just
- // assume the face is facing the camera, letting us skip any performance
- // issues or bugs in libtess's automatic calculation.
- const z = contours[0] ? contours[0][2] : undefined;
- let allSameZ = true;
- for (const contour of contours) {
- for (
- let j = 0;
- j < contour.length;
- j += this.tessyVertexSize
- ) {
- if (contour[j + 2] !== z) {
- allSameZ = false;
- break;
+ for (const h of holes) {
+ for (let j = 0; j < h.vertexData.length; j += this.tessyVertexSize) {
+ vertexData.push(h.vertexData.slice(j, j + this.tessyVertexSize));
}
}
+
+ for (const idx of indices) {
+ allTriangleVerts.push(...vertexData[idx]);
+ }
}
- if (allSameZ) {
- this._tessy.gluTessNormal(0, 0, 1);
- } else {
- // Let libtess pick a plane for us
- this._tessy.gluTessNormal(0, 0, 0);
+
+ return allTriangleVerts;
+ }
+
+ // Helper: Check if polygon is clockwise
+ _isClockwise(polygon) {
+ let sum = 0;
+ for (let i = 0; i < polygon.length; i++) {
+ const p1 = polygon[i];
+ const p2 = polygon[(i + 1) % polygon.length];
+ sum += (p2[0] - p1[0]) * (p2[1] + p1[1]);
}
-
- const triangleVerts = [];
- this._tessy.gluTessBeginPolygon(triangleVerts);
-
- for (const contour of contours) {
- this._tessy.gluTessBeginContour();
- for (
- let j = 0;
- j < contour.length;
- j += this.tessyVertexSize
- ) {
- const coords = contour.slice(
- j,
- j + this.tessyVertexSize
- );
- this._tessy.gluTessVertex(coords, coords);
- }
- this._tessy.gluTessEndContour();
+ return sum > 0;
+ }
+
+ // Helper: Check if outer contains a point
+ _contains(outerPolygon, [x, y]) {
+ let inside = false;
+ for (let i = 0, j = outerPolygon.length - 1; i < outerPolygon.length; j = i++) {
+ const [xi, yi] = outerPolygon[i];
+ const [xj, yj] = outerPolygon[j];
+
+ const intersect = ((yi > y) !== (yj > y)) &&
+ (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
+ if (intersect) inside = !inside;
}
-
- // finish polygon
- this._tessy.gluTessEndPolygon();
-
- return triangleVerts;
+ return inside;
}
-};
+}
\ No newline at end of file