Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for triangulation and concave polygon drawing #47

Merged
merged 1 commit into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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