From 924af7bb25e1a72ef91e912b11d9576d822e79a9 Mon Sep 17 00:00:00 2001 From: Perminder Date: Fri, 24 Jan 2025 04:51:49 +0530 Subject: [PATCH 1/4] fixing-text-model --- src/type/p5.Font.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index ffd984b829..fa40e127d9 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -119,16 +119,21 @@ function font(p5, fn) { const contours = this.textToContours(str, x, y, width, height, options); const geom = this._pInst.buildGeometry(() => { if (extrude === 0) { + this._pInst.push(); + this._pInst.rotateX(this._pInst.PI); + this._pInst.rotateZ(-this._pInst.HALF_PI); this._pInst.beginShape(); this._pInst.normal(0, 0, 1); for (const contour of contours) { this._pInst.beginContour(); for (const { x, y } of contour) { - this._pInst.vertex(x, y); + this._pInst.vertex(y, x); } this._pInst.endContour(this._pInst.CLOSE); } this._pInst.endShape(); + this._pInst.pop(); + } else { // Draw front faces for (const side of [1, -1]) { @@ -136,23 +141,30 @@ function font(p5, fn) { for (const contour of contours) { this._pInst.beginContour(); for (const { x, y } of contour) { - this._pInst.vertex(x, y, side * extrude * 0.5); + this._pInst.vertex(y, x, side * extrude * 0.5); } this._pInst.endContour(this._pInst.CLOSE); } + this._pInst.push(); + this._pInst.rotateX(this._pInst.PI); + this._pInst.rotateZ(-this._pInst.HALF_PI); this._pInst.endShape(); - this._pInst.beginShape(); + this._pInst.pop(); } // Draw sides + this._pInst.push(); + this._pInst.rotateX(this._pInst.PI); + this._pInst.rotateZ(-this._pInst.HALF_PI); for (const contour of contours) { this._pInst.beginShape(this._pInst.QUAD_STRIP); for (const v of contour) { for (const side of [-1, 1]) { - this._pInst.vertex(v.x, v.y, side * extrude * 0.5); + this._pInst.vertex(v.y, v.x, side * extrude * 0.5); } } this._pInst.endShape(); } + this._pInst.pop(); } }); if (extrude !== 0) { From 2b3e6f2d4286f0b6cead7f38f80d9c6de66a354b Mon Sep 17 00:00:00 2001 From: Perminder Date: Tue, 4 Feb 2025 08:04:30 +0530 Subject: [PATCH 2/4] earcut-implementation --- lib/empty-example/index.html | 6 +- lib/empty-example/sketch.js | 94 ++++++++++++++++- package-lock.json | 13 +-- package.json | 2 +- src/type/p5.Font.js | 26 ++--- src/webgl/ShapeBuilder.js | 192 ++++++++++++++++------------------- 6 files changed, 195 insertions(+), 138 deletions(-) diff --git a/lib/empty-example/index.html b/lib/empty-example/index.html index 56c88a89b8..780d01f0ce 100644 --- a/lib/empty-example/index.html +++ b/lib/empty-example/index.html @@ -12,9 +12,13 @@ background-color: #1b1b1b; } - + + + + + diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index c614f47b93..e953272a2f 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,7 +1,93 @@ -function setup() { - // put setup code here +let geom +let fonts +let artShader +let lineShader + +console.warn = () => {} + +OPC.text('words', 'WORD ART!') +OPC.select('font', ['Anton', 'Montserrat', 'Source Serif'], 'Anton') +OPC.slider({ name: 'warp', min: 0, max: 3, step: 0.01, value: 1 }) +OPC.slider({ name: 'extrude', min: 0, max: 20, step: 0.01, value: 5 }) +OPC.palette( + 'palette', + [ + ["#ffe03d", "#fe4830", "#d33033", "#6d358a", "#1c509e", "#00953c"], + ["#021d34", "#228fca", "#dcedf0"], + ["#044e9e", "#6190d3", "#fcf7ed", "#fcd494", "#f4b804"], + ["#0a0a0a", "#f7f3f2", "#0077e1", "#f5d216", "#fc3503"], + ] +) + +async function setup() { + createCanvas(800, 800, WEBGL) + fonts = { + Anton: await loadFont('https://fonts.gstatic.com/s/anton/v25/1Ptgg87LROyAm0K08i4gS7lu.ttf'), + Montserrat: await loadFont('https://fonts.gstatic.com/s/montserrat/v29/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-Y3tcoqK5.ttf'), + 'Source Serif': await loadFont('https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.ttf'), + } + + artShader = baseMaterialShader().modify({ + uniforms: { + 'float time': () => millis(), + 'float warp': () => warp, + 'float numColors': () => palette.length, + 'vec3[6] colors': () => palette.flatMap((c) => [red(c)/255, green(c)/255, blue(c)/255]), + }, + vertexDeclarations: 'out vec3 vPos;', + fragmentDeclarations: 'in vec3 vPos;', + 'Vertex getObjectInputs': `(Vertex inputs) { + vPos = inputs.position; + inputs.position.x += 5. * warp * sin(inputs.position.y*0.1 + time*0.001) / (1. + warp); + inputs.position.y += 5. * warp * sin(inputs.position.x*0.1 + time*0.0009) / (1. + warp); + return inputs; + }`, + 'vec4 getFinalColor': `(vec4 _c) { + float x = vPos.x * 0.005; + float a = floor(fract(x)*numColors); + float b = a == numColors-1. ? 0. : a + 1.; + float t = fract(x*numColors); + vec3 c = mix(colors[int(a)], colors[int(b)], t); + return vec4(c, 1.); + }` + }) + + lineShader = baseStrokeShader().modify({ + uniforms: { + 'float time': () => millis(), + 'float warp': () => warp, + }, + 'StrokeVertex getObjectInputs': `(StrokeVertex inputs) { + inputs.position.x += 5. * warp * sin(inputs.position.y*0.1 + time*0.001) / (1. + warp); + inputs.position.y += 5. * warp * sin(inputs.position.x*0.1 + time*0.0009) / (1. + warp); + return inputs; + }`, + }) } +let prevWords = '' +let prevFont = '' +let prevExtrude = -1 + function draw() { - // put drawing code here -} + if (words !== prevWords || prevFont !== font || prevExtrude !== extrude) { + if (geom) freeGeometry(geom) + + geom = fonts[font].textToModel(words, 0, 50, { sampleFactor: 2, extrude: 0 }); + geom.clearColors() + geom.normalize() + + prevWords = words + prevFont = font + prevExtrude = extrude + } + + background(255) + orbitControl() + // noStroke() + shader(artShader) + strokeShader(lineShader) + strokeWeight(4) + scale(min(width,height)/300) + model(geom) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3adf94f078..e988099e11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "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", - "libtess": "^1.2.2", "omggif": "^1.0.10", "pako": "^2.1.0" }, @@ -3913,6 +3913,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", @@ -6074,11 +6080,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==" - }, "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 978e7d8703..0fecc72732 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "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", - "libtess": "^1.2.2", "omggif": "^1.0.10", "pako": "^2.1.0" }, diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index fa40e127d9..68bacad8ad 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -119,21 +119,16 @@ function font(p5, fn) { const contours = this.textToContours(str, x, y, width, height, options); const geom = this._pInst.buildGeometry(() => { if (extrude === 0) { - this._pInst.push(); - this._pInst.rotateX(this._pInst.PI); - this._pInst.rotateZ(-this._pInst.HALF_PI); this._pInst.beginShape(); this._pInst.normal(0, 0, 1); for (const contour of contours) { this._pInst.beginContour(); for (const { x, y } of contour) { - this._pInst.vertex(y, x); + this._pInst.vertex(x, y); } - this._pInst.endContour(this._pInst.CLOSE); + this._pInst.endContour(); } this._pInst.endShape(); - this._pInst.pop(); - } else { // Draw front faces for (const side of [1, -1]) { @@ -141,30 +136,23 @@ function font(p5, fn) { for (const contour of contours) { this._pInst.beginContour(); for (const { x, y } of contour) { - this._pInst.vertex(y, x, side * extrude * 0.5); + this._pInst.vertex(x, y, side * extrude * 0.5); } - this._pInst.endContour(this._pInst.CLOSE); + this._pInst.endContour(); } - this._pInst.push(); - this._pInst.rotateX(this._pInst.PI); - this._pInst.rotateZ(-this._pInst.HALF_PI); this._pInst.endShape(); - this._pInst.pop(); + this._pInst.beginShape(); } // Draw sides - this._pInst.push(); - this._pInst.rotateX(this._pInst.PI); - this._pInst.rotateZ(-this._pInst.HALF_PI); for (const contour of contours) { this._pInst.beginShape(this._pInst.QUAD_STRIP); for (const v of contour) { for (const side of [-1, 1]) { - this._pInst.vertex(v.y, v.x, side * extrude * 0.5); + this._pInst.vertex(v.x, v.y, side * extrude * 0.5); } } this._pInst.endShape(); } - this._pInst.pop(); } }); if (extrude !== 0) { @@ -804,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 From 501af5011f450dce8762adff279368f1fa16f7e5 Mon Sep 17 00:00:00 2001 From: Perminder Date: Tue, 4 Feb 2025 08:06:35 +0530 Subject: [PATCH 3/4] fixing --- lib/empty-example/index.html | 6 +-- lib/empty-example/sketch.js | 100 +++-------------------------------- 2 files changed, 8 insertions(+), 98 deletions(-) diff --git a/lib/empty-example/index.html b/lib/empty-example/index.html index 780d01f0ce..54b1bfdfe2 100644 --- a/lib/empty-example/index.html +++ b/lib/empty-example/index.html @@ -12,13 +12,9 @@ background-color: #1b1b1b; } - + - - - - diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index e953272a2f..3b725f9da9 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,93 +1,7 @@ -let geom -let fonts -let artShader -let lineShader - -console.warn = () => {} - -OPC.text('words', 'WORD ART!') -OPC.select('font', ['Anton', 'Montserrat', 'Source Serif'], 'Anton') -OPC.slider({ name: 'warp', min: 0, max: 3, step: 0.01, value: 1 }) -OPC.slider({ name: 'extrude', min: 0, max: 20, step: 0.01, value: 5 }) -OPC.palette( - 'palette', - [ - ["#ffe03d", "#fe4830", "#d33033", "#6d358a", "#1c509e", "#00953c"], - ["#021d34", "#228fca", "#dcedf0"], - ["#044e9e", "#6190d3", "#fcf7ed", "#fcd494", "#f4b804"], - ["#0a0a0a", "#f7f3f2", "#0077e1", "#f5d216", "#fc3503"], - ] -) - -async function setup() { - createCanvas(800, 800, WEBGL) - fonts = { - Anton: await loadFont('https://fonts.gstatic.com/s/anton/v25/1Ptgg87LROyAm0K08i4gS7lu.ttf'), - Montserrat: await loadFont('https://fonts.gstatic.com/s/montserrat/v29/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-Y3tcoqK5.ttf'), - 'Source Serif': await loadFont('https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.ttf'), - } - - artShader = baseMaterialShader().modify({ - uniforms: { - 'float time': () => millis(), - 'float warp': () => warp, - 'float numColors': () => palette.length, - 'vec3[6] colors': () => palette.flatMap((c) => [red(c)/255, green(c)/255, blue(c)/255]), - }, - vertexDeclarations: 'out vec3 vPos;', - fragmentDeclarations: 'in vec3 vPos;', - 'Vertex getObjectInputs': `(Vertex inputs) { - vPos = inputs.position; - inputs.position.x += 5. * warp * sin(inputs.position.y*0.1 + time*0.001) / (1. + warp); - inputs.position.y += 5. * warp * sin(inputs.position.x*0.1 + time*0.0009) / (1. + warp); - return inputs; - }`, - 'vec4 getFinalColor': `(vec4 _c) { - float x = vPos.x * 0.005; - float a = floor(fract(x)*numColors); - float b = a == numColors-1. ? 0. : a + 1.; - float t = fract(x*numColors); - vec3 c = mix(colors[int(a)], colors[int(b)], t); - return vec4(c, 1.); - }` - }) - - lineShader = baseStrokeShader().modify({ - uniforms: { - 'float time': () => millis(), - 'float warp': () => warp, - }, - 'StrokeVertex getObjectInputs': `(StrokeVertex inputs) { - inputs.position.x += 5. * warp * sin(inputs.position.y*0.1 + time*0.001) / (1. + warp); - inputs.position.y += 5. * warp * sin(inputs.position.x*0.1 + time*0.0009) / (1. + warp); - return inputs; - }`, - }) -} - -let prevWords = '' -let prevFont = '' -let prevExtrude = -1 - -function draw() { - if (words !== prevWords || prevFont !== font || prevExtrude !== extrude) { - if (geom) freeGeometry(geom) - - geom = fonts[font].textToModel(words, 0, 50, { sampleFactor: 2, extrude: 0 }); - geom.clearColors() - geom.normalize() - - prevWords = words - prevFont = font - prevExtrude = extrude - } - - background(255) - orbitControl() - // noStroke() - shader(artShader) - strokeShader(lineShader) - strokeWeight(4) - scale(min(width,height)/300) - model(geom) -} \ No newline at end of file +function setup() { + // put setup code here + } + + function draw() { + // put drawing code here + } \ No newline at end of file From c683222ef6b66d4204377ad6f5c9a98fb944ec86 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Wed, 12 Feb 2025 02:38:40 +0530 Subject: [PATCH 4/4] fixing-for-3d-cases --- src/webgl/ShapeBuilder.js | 152 ++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 54 deletions(-) diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index bcb2b3adb7..fd5aac1cb5 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -285,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 = []; @@ -296,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))); { @@ -371,92 +367,140 @@ export class ShapeBuilder { _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]]); + } + } + // Compute a projection basis from all points. + const basis = this._computeProjectionBasis(allPoints3D); - // 1. Classify contours as outer shapes or holes using winding order - const classifiedContours = contours.map(contour => { + // (B) For each contour, build its 2D projection. + let classifiedContours = contours.map(contour => { const polygon = []; - for (let j = 0; j < contour.length; j += this.tessyVertexSize) { - polygon.push([contour[j], contour[j + 1]]); + 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); } return { - isHole: this._isClockwise(polygon), + // We will decide later if this contour is outer or a hole. 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: [] - }); + 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; + } + } + if (!contained) { + outerContours.push(c); } 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); + holeContours.push(c); } } + // 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])) + })); - // 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)); + 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 += this.tessyVertexSize) { - vertexData.push(h.vertexData.slice(j, j + this.tessyVertexSize)); + 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(...vertexData[idx]); + 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)]; + }; - // 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]); + _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] + }; } - return sum > 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]); + } + let len = Math.hypot(normal[0], normal[1], normal[2]); + if (len === 0) { + normal = [0, 0, 1]; + } else { + normal = normal.map(n => n / len); + } + // 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]]; + } + 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 }; + }; - // 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; } return inside; } -} \ No newline at end of file +};