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