Skip to content

Commit

Permalink
Support for triangulation and concave polygon drawing
Browse files Browse the repository at this point in the history
  • Loading branch information
Marc Flerackers committed May 25, 2024
1 parent 2e10aa7 commit 319b3e4
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- added global raycast function and raycast method to level
- added support for textured polygons
- added support for concave polygon drawing
- added `loadMusic()` to load streaming audio (doesn't block in loading screen)
- added support for arrays in uniforms
- added support for texture larger than 2048x2048
Expand Down
23 changes: 23 additions & 0 deletions examples/polygon.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ const poly = add([
vec2(80, 80),
vec2(-60, 120),
vec2(-120, 0),
vec2(-200, 20),
vec2(-270, 60),
], {
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),
rgb(255, 128, 128),
],
}),
pos(300, 300),
Expand All @@ -33,6 +37,16 @@ let dragging = null;
let hovering = null;

poly.onDraw(() => {
const triangles = triangulate(poly.pts)
for (const triangle of triangles) {
drawTriangle({
p1: triangle[0],
p2: triangle[1],
p3: triangle[2],
fill: false,
outline: { color: BLACK }
})
}
if (hovering !== null) {
drawCircle({
pos: poly.pts[hovering],
Expand All @@ -41,6 +55,15 @@ poly.onDraw(() => {
}
});

onUpdate(()=>{
if (isConvex(poly.pts)) {
poly.color = WHITE
}
else {
poly.color = rgb(192, 192, 192)
}
})

onMousePress(() => {
dragging = hovering;
});
Expand Down
20 changes: 17 additions & 3 deletions src/kaboom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ import {
Vec2,
vec2,
wave,
isConvex,
triangulate,
} from "./math";

import easings from "./easings";
Expand Down Expand Up @@ -2196,9 +2198,17 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => {
}));

// TODO: better triangulation
const indices = [...Array(npts - 2).keys()]
let indices;

if (opt.triangulate && isConvex(opt.pts)) {
const triangles = triangulate(opt.pts)
indices = triangles.map(t => t.map(p => opt.pts.indexOf(p))).flat()
}
else {
indices = [...Array(npts - 2).keys()]
.map((n) => [0, n + 1, n + 2])
.flat();
}

drawRaw(
verts,
Expand Down Expand Up @@ -3859,8 +3869,10 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => {
},

inspect() {
return `(${(this.area.scale.x).toFixed(1)}, ${(this.area.scale.y).toFixed(1)})`;
}
return `(${this.area.scale.x.toFixed(1)}, ${
this.area.scale.y.toFixed(1)
})`;
},
};
}

Expand Down Expand Up @@ -6848,6 +6860,8 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => {
testCirclePolygon,
testLinePoint,
testLineCircle,
isConvex,
triangulate,
// raw draw
drawSprite,
drawText,
Expand Down
148 changes: 148 additions & 0 deletions src/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2334,3 +2334,151 @@ export function sat(p1: Polygon, p2: Polygon): Vec2 | null {
}
return displacement;
}

// true if the angle is oriented counter clockwise
function isOrientedCcw(a: Vec2, b: Vec2, c: Vec2) {
// return det(b-a, c-a) >= 0
return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) >= 0;
}

// true if the polygon is oriented counter clockwise
function isOrientedCcwPolygon(polygon: Vec2[]) {
let total =0;
let prev:Vec2 = polygon[polygon.length-1];
for (let i = 0; i < polygon.length; i++) {
total += (polygon[i].x - prev.x) * (polygon[i].y + prev.y);
prev = polygon[i];
}
return total < 0;
}

// true if a and b are on the same side of the line c->d
function onSameSide(a: Vec2, b: Vec2, c: Vec2, d: Vec2) {
const px = d.x - c.x, py = d.y - c.y;
// return det(p, a-c) * det(p, b-c) >= 0
const l = px * (a.y - c.y) - py * (a.x - c.x);
const m = px * (b.y - c.y) - py * (b.x - c.x);
return l * m >= 0;
}

// true if p is contained in the triangle abc
function pointInTriangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) {
return onSameSide(p, a, b, c) && onSameSide(p, b, a, c)
&& onSameSide(p, c, a, b);
}

// 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) {
if (
(p !== a) && (p !== b) && (p !== c) && pointInTriangle(p, a, b, c)
) {
return true;
}
}

return false;
}

// true if the triangle is an ear, which is whether it can be cut off from the polygon without leaving a hole behind
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];
}

/* 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 = idx;
}
nextIdx[i] = i + 1;
prevIdx[i] = i - 1;
}
nextIdx[nextIdx.length - 1] = 0;
prevIdx[0] = prevIdx.length - 1;

// 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]);
}
}

const triangles = [];
let nVertices = pts.length;
let current = 1;
let skipped = 0;
let next;
let prev;
while (nVertices > 3) {
next = nextIdx[current];
prev = prevIdx[current];
const a = pts[prev];
const b = pts[current];
const c = pts[next];
if (isEar(a, b, c, concaveVertices)) {
triangles.push([a, b, c]);
nextIdx[prev] = next;
prevIdx[next] = prev;
concaveVertices.splice(concaveVertices.indexOf(b), 1);
--nVertices;
skipped = 0;
} else if (++skipped > nVertices) {
return [];
}
current = next;
}
next = nextIdx[current];
prev = prevIdx[current];
triangles.push([pts[prev], pts[current], pts[next]]);

return triangles;
}

export function isConvex(pts: Vec2[])
{
if (pts.length < 3)
return false;

// a polygon is convex if all corners turn in the same direction
// turning direction can be determined using the cross-product of
// the forward difference vectors
let i = pts.length - 2
let j = pts.length - 1
let k = 0;
let p = pts[j].sub(pts[i]);
let q = pts[k].sub(pts[j]);
let winding = p.cross(q);

while (k+1 < pts.length)
{
i = j;
j = k;
k++;
p = pts[j].sub(pts[i]);
q = pts[k].sub(pts[j]);

if (p.cross(q) * winding < 0) {
return false;
}
}
return true;
}
12 changes: 10 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,8 @@ export interface KaboomCtx {
* Check if a circle and polygon intersect linewise.
*/
testCirclePolygon(c: Circle, p: Polygon): boolean;
isConvex(pts:Vec2[]): boolean;
triangulate(pts:Vec2[]): Vec2[][];
Line: typeof Line;
Rect: typeof Rect;
Circle: typeof Circle;
Expand Down Expand Up @@ -3865,6 +3867,12 @@ export type DrawPolygonOpt = RenderProps & {
* @since v3001.0
*/
tex?: Texture;
/**
* Triangulate concave polygons.
*
* @since v3001.0
*/
triangulate?: boolean
};

export interface Outline {
Expand Down Expand Up @@ -5065,13 +5073,13 @@ export interface PolygonComp extends Comp {
colors?: Color[];
/**
* The uv of each vertex.
*
*
* @since v3001.0
*/
uv?: Vec2[];
/**
* The texture used when uv coordinates are present.
*
*
* @since v3001.0
*/
tex?: Texture;
Expand Down

0 comments on commit 319b3e4

Please sign in to comment.