diff --git a/CHANGELOG.md b/CHANGELOG.md index 799611287..4b17eedd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,11 @@ So your change should look like: ## [unreleased] +### Breaking Changes + +- `triangulate()` now returns a flat list of the new indices, instead of a list + of 3-tuples of points (#1103) - @dragoncoder047 + ### Added - Made random generator algorithm configurable using `setRNG()` (#1057) - @@ -59,6 +64,8 @@ So your change should look like: ### Fixed +- Fixed `triangulate()` returning invalid triangles in some cases (#1093) - + @dragoncoder047 - Fixed `TimerController.timeLeft` returning elapsed time instead of remaining time (#1082) - @nojaf - Fixed mouse coordinates not being calculated properly when canvas is resized @@ -523,10 +530,12 @@ So your change should look like: - Now, you can use `color(c)` with a hexadecimal literal number (ex: 0x00ff00) - @lajbel + ```js // blue frog add([sprite("bean"), color(0x0000ff)]); ``` + - **(!)** `KAPLAYCtx` doesn't use generics anymore. Now, `KAPLAYCtxT` uses them - @lajbel - Now, `kaplay` will return `KAPLAYCtx` or `KAPLAYCtxT` depending if it's using diff --git a/examples/polygon.js b/examples/polygon.js index 1fb2de6c0..2e82e3bf5 100644 --- a/examples/polygon.js +++ b/examples/polygon.js @@ -49,12 +49,12 @@ let dragging = null; let hovering = null; poly.onDraw(() => { - const triangles = triangulate(poly.pts); - for (const triangle of triangles) { + const triangleIndices = triangulate(poly.pts); + for (let i = 0; i < triangleIndices.length; i += 3) { drawTriangle({ - p1: triangle[0], - p2: triangle[1], - p3: triangle[2], + p1: poly.pts[triangleIndices[i]], + p2: poly.pts[triangleIndices[i + 1]], + p3: poly.pts[triangleIndices[i + 2]], fill: false, outline: { color: BLACK }, }); diff --git a/src/core/contextType.ts b/src/core/contextType.ts index 51c6c9f14..569b22979 100644 --- a/src/core/contextType.ts +++ b/src/core/contextType.ts @@ -6432,8 +6432,9 @@ export interface KAPLAYCtx { * @since v3001.0 * @group Math * @subgroup Advanced + * @returns flattened list of indices of each of the decomposed triangles, list will always have a multiple of 3 length */ - triangulate(pts: Vec2[]): Vec2[][]; + triangulate(pts: Vec2[]): number[]; /** * @returns the convex hull of the given polygon. * @since v4000.0 diff --git a/src/gfx/draw/drawPolygon.ts b/src/gfx/draw/drawPolygon.ts index 09bdefa2f..91c35bfcd 100644 --- a/src/gfx/draw/drawPolygon.ts +++ b/src/gfx/draw/drawPolygon.ts @@ -156,10 +156,7 @@ export function drawPolygon(opt: DrawPolygonOpt) { let indices; if (opt.triangulate /* && !isConvex(opt.pts)*/) { - const triangles = triangulate(opt.pts); - // TODO rewrite triangulate to just return new indices - indices = triangles.map(t => t.map(p => opt.pts.indexOf(p))) - .flat(); + indices = triangulate(opt.pts); } else { indices = [...Array(npts - 2).keys()] diff --git a/src/math/math.ts b/src/math/math.ts index 25c09bb53..6bff75130 100644 --- a/src/math/math.ts +++ b/src/math/math.ts @@ -3257,7 +3257,8 @@ function pointInTriangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) { // true if any vertex in the list `vertices' is in the triangle abc. function someInTriangle(vertices: Vec2[], a: Vec2, b: Vec2, c: Vec2) { - for (const p of vertices) { + for (let i = 0; i < vertices.length; i++) { + const p = vertices[i]!; if ( (p !== a) && (p !== b) && (p !== c) && pointInTriangle(p, a, b, c) ) { @@ -3273,41 +3274,24 @@ function isEar(a: Vec2, b: Vec2, c: Vec2, vertices: Vec2[]) { return isOrientedCcw(a, b, c) && !someInTriangle(vertices, a, b, c); } -export function triangulate(pts: Vec2[]): Vec2[][] { - if (pts.length < 3) { - return []; - } - if (pts.length == 3) { - return [pts]; +export function triangulate(pts: Vec2[]): number[] { + const len = pts.length; + + if (len < 3) return []; + if (len === 3) { + return [0, 1, 2]; } /* Create a list of indexes to the previous and next points of a given point prev_idx[i] gives the index to the previous point of the point at i */ - let nextIdx = []; - let prevIdx = []; - let idx = 0; - for (let i = 0; i < pts.length; i++) { - const lm = pts[idx]; - const pt = pts[i]; - if (pt.x < lm.x || (pt.x == lm.x && pt.y < lm.y)) { - idx = i; - } - nextIdx[i] = i + 1; - prevIdx[i] = i - 1; - } - nextIdx[nextIdx.length - 1] = 0; - prevIdx[0] = prevIdx.length - 1; + let prevIdx = pts.map((_, i) => (i + len - 1) % len); + let nextIdx = pts.map((_, i) => (i + 1) % len); // If the polygon is not counter clockwise, swap the lists, thus reversing the winding if (!isOrientedCcwPolygon(pts)) { - [nextIdx, prevIdx] = [prevIdx, nextIdx]; - } - - const concaveVertices = []; - for (let i = 0; i < pts.length; ++i) { - if (!isOrientedCcw(pts[prevIdx[i]], pts[i], pts[nextIdx[i]])) { - concaveVertices.push(pts[i]); - } + let temp = prevIdx; + prevIdx = nextIdx; + nextIdx = temp; } const triangles = []; @@ -3322,11 +3306,10 @@ export function triangulate(pts: Vec2[]): Vec2[][] { const a = pts[prev]; const b = pts[current]; const c = pts[next]; - if (isEar(a, b, c, concaveVertices)) { - triangles.push([a, b, c]); + if (isEar(a, b, c, pts)) { + triangles.push(prev, current, next); nextIdx[prev] = next; prevIdx[next] = prev; - concaveVertices.splice(concaveVertices.indexOf(b), 1); --nVertices; skipped = 0; } @@ -3337,7 +3320,7 @@ export function triangulate(pts: Vec2[]): Vec2[][] { } next = nextIdx[current]; prev = prevIdx[current]; - triangles.push([pts[prev], pts[current], pts[next]]); + triangles.push(prev, current, next); return triangles; } diff --git a/tests/playtests/hyperconcave_polygon.js b/tests/playtests/hyperconcave_polygon.js new file mode 100644 index 000000000..a6e5193a3 --- /dev/null +++ b/tests/playtests/hyperconcave_polygon.js @@ -0,0 +1,103 @@ +kaplay({ + background: [0, 0, 0], +}); + +add([ + text("Drag corners of the polygon"), + pos(20, 20), +]); + +// Make a weird shape +const poly = add([ + polygon([ + vec2(0, 0), + vec2(100, 0), + vec2(200, 100), + vec2(100, 200), + vec2(0, 100), + vec2(80, 100), + ], { + colors: [ + rgb(128, 255, 128), + rgb(255, 128, 128), + rgb(128, 128, 255), + rgb(255, 128, 128), + rgb(128, 128, 128), + rgb(128, 255, 128), + ], + triangulate: true, + }), + pos(150, 150), + area(), + color(), +]); + +let dragging = null; +let hovering = null; + +poly.onDraw(() => { + const triangleIndices = triangulate(poly.pts); + for (var i = 0; i < triangleIndices.length; i += 3) { + drawTriangle({ + p1: poly.pts[triangleIndices[i]], + p2: poly.pts[triangleIndices[i + 1]], + p3: poly.pts[triangleIndices[i + 2]], + fill: false, + outline: { color: BLACK }, + }); + } + if (hovering !== null) { + drawCircle({ + pos: poly.pts[hovering], + radius: 16, + }); + } +}); + +onUpdate(() => { + if (isConvex(poly.pts)) { + poly.color = WHITE; + } + else { + poly.color = rgb(192, 192, 192); + } +}); + +onMousePress(() => { + dragging = hovering; +}); + +onMouseRelease(() => { + dragging = null; +}); + +onMouseMove(() => { + hovering = null; + const mp = mousePos().sub(poly.pos); + for (let i = 0; i < poly.pts.length; i++) { + if (mp.dist(poly.pts[i]) < 16) { + hovering = i; + break; + } + } + if (dragging !== null) { + poly.pts[dragging] = mousePos().sub(poly.pos); + } +}); + +poly.onHover(() => { + poly.color = rgb(200, 200, 255); +}); + +poly.onHoverEnd(() => { + poly.color = rgb(255, 255, 255); +}); + +poly.onDraw(() => { + drawPolygon({ + ...poly, + pos: vec2(), + fill: false, + outline: { color: RED, width: 5 }, + }); +});