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..fd5aac1cb5 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,
@@ -293,7 +285,6 @@ export class ShapeBuilder {
contours[contours.length-1].push(...vals);
}
}
-
const polyTriangles = this._triangulate(contours);
const originalVertices = this.geometry.vertices;
this.geometry.vertices = [];
@@ -304,11 +295,8 @@ export class ShapeBuilder {
prop.resetSrcArray();
}
const colors = [];
- for (
- let j = 0, polyTriLength = polyTriangles.length;
- j < polyTriLength;
- j = j + this.tessyVertexSize
- ) {
+ // Loop over each vertex (remember: each triangle vertex is packed in tessyVertexSize floats)
+ for (let j = 0, len = polyTriangles.length; j < len; j += this.tessyVertexSize) {
colors.push(...polyTriangles.slice(j + 5, j + 9));
this.geometry.vertexNormals.push(new Vector(...polyTriangles.slice(j + 9, j + 12)));
{
@@ -377,108 +365,142 @@ 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 = [];
+ const vertexSize = this.tessyVertexSize;
+
+ // (A) Collect all 3D points from every contour.
+ const allPoints3D = [];
+ for (const contour of contours) {
+ for (let j = 0; j < contour.length; j += vertexSize) {
+ allPoints3D.push([contour[j], contour[j + 1], contour[j + 2]]);
}
}
-
- function begincallback(type) {
- if (type !== libtess.primitiveType.GL_TRIANGLES) {
- console.log(`expected TRIANGLES but got type: ${type}`);
+ // Compute a projection basis from all points.
+ const basis = this._computeProjectionBasis(allPoints3D);
+
+ // (B) For each contour, build its 2D projection.
+ let classifiedContours = contours.map(contour => {
+ const polygon = [];
+ for (let j = 0; j < contour.length; j += vertexSize) {
+ const pt3 = [contour[j], contour[j + 1], contour[j + 2]];
+ const pt2 = this._projectPoint(pt3, basis);
+ polygon.push(pt2);
}
- }
-
- 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];
+ return {
+ // We will decide later if this contour is outer or a hole.
+ polygon,
+ vertexData: contour
+ };
+ });
+
+ const outerContours = [];
+ const holeContours = [];
+ for (let i = 0; i < classifiedContours.length; i++) {
+ const c = classifiedContours[i];
+ let contained = false;
+ for (let j = 0; j < classifiedContours.length; j++) {
+ if (i === j) continue;
+ if (this._contains(classifiedContours[j].polygon, c.polygon[0])) {
+ contained = true;
+ break;
}
}
- return result;
- };
-
- function edgeCallback(flag) {
- // don't really care about the flag, but need no-strip/no-fan behavior
+ if (!contained) {
+ outerContours.push(c);
+ } else {
+ holeContours.push(c);
+ }
}
-
- 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;
+ // Group each outer contour with all holes contained in it.
+ const contourGroups = outerContours.map(outer => ({
+ outer,
+ holes: holeContours.filter(hole => this._contains(outer.polygon, hole.polygon[0]))
+ }));
+
+ for (const group of contourGroups) {
+ const { outer, holes } = group;
+ const polygons = [outer.polygon, ...holes.map(h => h.polygon)];
+ const { vertices: verts2D, holes: earcutHoles, dimensions } = flatten(polygons);
+ const indices = earcut(verts2D, earcutHoles, dimensions);
+
+ const vertexDataChunks = [];
+ for (let j = 0; j < outer.vertexData.length; j += vertexSize) {
+ vertexDataChunks.push(outer.vertexData.slice(j, j + vertexSize));
+ }
+ // Then add the holes’ vertexData (in the same order as flatten() does).
+ for (const h of holes) {
+ for (let j = 0; j < h.vertexData.length; j += vertexSize) {
+ vertexDataChunks.push(h.vertexData.slice(j, j + vertexSize));
}
}
+ // Build triangles using earcut’s indices.
+ for (const idx of indices) {
+ allTriangleVerts.push(...vertexDataChunks[idx]);
+ }
+ }
+ return allTriangleVerts;
+ };
+
+ _projectPoint(pt, basis) {
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ return [dot(pt, basis.u), dot(pt, basis.v)];
+ };
+
+ _computeProjectionBasis(points) {
+ const epsilon = 1e-6;
+ const firstZ = points[0][2];
+ const isFlat2D = points.every(p => Math.abs(p[2] - firstZ) < epsilon);
+ if (isFlat2D) {
+ return {
+ normal: [0, 0, 1],
+ u: [1, 0, 0],
+ v: [0, 1, 0]
+ };
+ }
+ // Otherwise compute a normal via Newell's method.
+ let normal = [0, 0, 0];
+ for (let i = 0; i < points.length; i++) {
+ const current = points[i];
+ const next = points[(i + 1) % points.length];
+ normal[0] += (current[1] - next[1]) * (current[2] + next[2]);
+ normal[1] += (current[2] - next[2]) * (current[0] + next[0]);
+ normal[2] += (current[0] - next[0]) * (current[1] + next[1]);
}
- if (allSameZ) {
- this._tessy.gluTessNormal(0, 0, 1);
+ let len = Math.hypot(normal[0], normal[1], normal[2]);
+ if (len === 0) {
+ normal = [0, 0, 1];
} else {
- // Let libtess pick a plane for us
- this._tessy.gluTessNormal(0, 0, 0);
+ normal = normal.map(n => n / len);
}
-
- 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();
+ // Choose an arbitrary vector not parallel to the normal.
+ let u;
+ if (Math.abs(normal[0]) > Math.abs(normal[1])) {
+ u = [-normal[2], 0, normal[0]];
+ } else {
+ u = [0, normal[2], -normal[1]];
}
-
- // finish polygon
- this._tessy.gluTessEndPolygon();
-
- return triangleVerts;
+ let uLen = Math.hypot(u[0], u[1], u[2]);
+ if (uLen === 0) u = [1, 0, 0];
+ else u = u.map(x => x / uLen);
+ // Compute v = normal cross u.
+ const v = [
+ normal[1] * u[2] - normal[2] * u[1],
+ normal[2] * u[0] - normal[0] * u[2],
+ normal[0] * u[1] - normal[1] * u[0]
+ ];
+ return { normal, u, v };
+ };
+
+ _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;
+ }
+ return inside;
}
};