### BSP Tree

A Binary Space Partitioning Tree (BSP Tree) baseline implementation adapted to turtletoy

- resolves all z-index conflicts by splitting polygons
- tree can be traversed in perfect front-to-back order, then using Reinder's polygon's occlusion/clipping method

```// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);
const turtle = new Turtle();
turtle.penup();
/* ===================================================
*  ------------ BSP Tree  -------------
* adapted from C++ BSP Tree Demo
* http://www.gamers.org/dhs/helpdocs/bsp_faq.html
* =================================================== */

const polygons = Polygons();

///////////////////////// vec 4 /////////////////////////
const v4 = {
multiplyScalar (v, s) {
return [
v[0] * s,
v[1] * s,
v[2] * s,
v[3] * s
];
},
subtract (v, p) {
return [
v[0] - p[0],
v[1] - p[1],
v[2] - p[2],
v[3] - p[3]
];
},
return [
v[0] + p[0],
v[1] + p[1],
v[2] + p[2],
v[3] + p[3]
];
},
multiplyMatrix (v, m) {
const px = v[0], py = v[1], pz = v[2], pw = v[3];
return [
px * m[0] + py * m[4] + pz * m[8] +  pw * m[12],
px * m[1] + py * m[5] + pz * m[9] +  pw * m[13],
px * m[2] + py * m[6] + pz * m[10] + pw * m[14],
px * m[3] + py * m[7] + pz * m[11] + pw * m[15]
];
},
crossProduct (v, p) {
return [
v[1] * p[2] - v[2] * p[1],
v[2] * p[0] - v[0] * p[2],
v[0] * p[1] - v[1] * p[0],
0
];
},
dotProduct (v, p) {
return (
v[0] * p[0] +
v[1] * p[1] +
v[2] * p[2] +
v[3] * p[3]
);
},
normalize (v) {
const n = Math.sqrt(
v[0] * v[0] +
v[1] * v[1] +
v[2] * v[2] +
v[3] * v[3]
);
return [
v[0] / n,
v[1] / n,
v[2] / n,
v[3] / n
];
}
};
///////////////////////// mat 4 ///////////////////////
const m4 = {
identity() {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
},
multiply (m, n) {
const
m1XX = m[0], m1XY = m[1], m1XZ = m[2], m1XW = m[3],
m1YX = m[4], m1YY = m[5], m1YZ = m[6], m1YW = m[7],
m1ZX = m[8], m1ZY = m[9], m1ZZ = m[10], m1ZW = m[11],
m1WX = m[12], m1WY = m[13], m1WZ = m[14], m1WW = m[15],
m2XX = n[0], m2XY = n[1], m2XZ = n[2], m2XW = n[3],
m2YX = n[4], m2YY = n[5], m2YZ = n[6], m2YW = n[7],
m2ZX = n[8], m2ZY = n[9], m2ZZ = n[10], m2ZW = n[11],
m2WX = n[12], m2WY = n[13], m2WZ = n[14], m2WW = n[15];
return [
m1XX * m2XX + m1XY * m2YX + m1XZ * m2ZX + m1XW * m2WX,
m1XX * m2XY + m1XY * m2YY + m1XZ * m2ZY + m1XW * m2WY,
m1XX * m2XZ + m1XY * m2YZ + m1XZ * m2ZZ + m1XW * m2WZ,
m1XX * m2XW + m1XY * m2YW + m1XZ * m2ZW + m1XW * m2WW,
m1YX * m2XX + m1YY * m2YX + m1YZ * m2ZX + m1YW * m2WX,
m1YX * m2XY + m1YY * m2YY + m1YZ * m2ZY + m1YW * m2WY,
m1YX * m2XZ + m1YY * m2YZ + m1YZ * m2ZZ + m1YW * m2WZ,
m1YX * m2XW + m1YY * m2YW + m1YZ * m2ZW + m1YW * m2WW,
m1ZX * m2XX + m1ZY * m2YX + m1ZZ * m2ZX + m1ZW * m2WX,
m1ZX * m2XY + m1ZY * m2YY + m1ZZ * m2ZY + m1ZW * m2WY,
m1ZX * m2XZ + m1ZY * m2YZ + m1ZZ * m2ZZ + m1ZW * m2WZ,
m1ZX * m2XW + m1ZY * m2YW + m1ZZ * m2ZW + m1ZW * m2WW,
m1WX * m2XX + m1WY * m2YX + m1WZ * m2ZX + m1WW * m2WX,
m1WX * m2XY + m1WY * m2YY + m1WZ * m2ZY + m1WW * m2WY,
m1WX * m2XZ + m1WY * m2YZ + m1WZ * m2ZZ + m1WW * m2WZ,
m1WX * m2XW + m1WY * m2YW + m1WZ * m2ZW + m1WW * m2WW
];
},
inverse (m) {
const
a00 = m[0],  a01 = m[1],  a02 = m[2],  a03 = m[3],
a10 = m[4],  a11 = m[5],  a12 = m[6],  a13 = m[7],
a20 = m[8],  a21 = m[9],  a22 = m[10], a23 = m[11],
a30 = m[12], a31 = m[13], a32 = m[14], a33 = m[15],
b00 = a00 * a11 - a01 * a10,
b01 = a00 * a12 - a02 * a10,
b02 = a00 * a13 - a03 * a10,
b03 = a01 * a12 - a02 * a11,
b04 = a01 * a13 - a03 * a11,
b05 = a02 * a13 - a03 * a12,
b06 = a20 * a31 - a21 * a30,
b07 = a20 * a32 - a22 * a30,
b08 = a20 * a33 - a23 * a30,
b09 = a21 * a32 - a22 * a31,
b10 = a21 * a33 - a23 * a31,
b11 = a22 * a33 - a23 * a32;
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if (!det) return this;
det = 1.0 / det;
return [
(a11 * b11 - a12 * b10 + a13 * b09) * det,
(a02 * b10 - a01 * b11 - a03 * b09) * det,
(a31 * b05 - a32 * b04 + a33 * b03) * det,
(a22 * b04 - a21 * b05 - a23 * b03) * det,
(a12 * b08 - a10 * b11 - a13 * b07) * det,
(a00 * b11 - a02 * b08 + a03 * b07) * det,
(a32 * b02 - a30 * b05 - a33 * b01) * det,
(a20 * b05 - a22 * b02 + a23 * b01) * det,
(a10 * b10 - a11 * b08 + a13 * b06) * det,
(a01 * b08 - a00 * b10 - a03 * b06) * det,
(a30 * b04 - a31 * b02 + a33 * b00) * det,
(a21 * b02 - a20 * b04 - a23 * b00) * det,
(a11 * b07 - a10 * b09 - a12 * b06) * det,
(a00 * b09 - a01 * b07 + a02 * b06) * det,
(a31 * b01 - a30 * b03 - a32 * b00) * det,
(a20 * b03 - a21 * b01 + a22 * b00) * det
];
},
rotateXYZ (angleX, angleY, angleZ) {
const cw = Math.cos(angleX),
sw = Math.sin(angleX),
cy = Math.cos(angleY),
sy = Math.sin(angleY),
ck = angleZ ? Math.cos(angleZ) : 1,
sk = angleZ ? Math.sin(angleZ) : 0;
return [
cy*ck, cw*sk+sw*sy*ck, sw*sk-cw*sy*ck, 0,
-cy*sk, cw*ck-sw*sy*sk, sw*ck+cw*sy*sk, 0,
sy, -sw*cy, cw*cy, 0,
0, 0, 0, 1
];
},
scale (m, x, y, z) {
return this.multiply(m,
[
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1
]
);
},
translate (m, x, y, z) {
return this.multiply(m,
[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1
]
);
},
rotateX (m, angle) {
angle = angle * (Math.PI / 180);
const c = Math.cos(angle),
s = Math.sin(angle);
return this.multiply(m,
[
1, 0, 0, 0,
0, c, s, 0,
0,-s, c, 0,
0, 0, 0, 1
]
);
},
rotateY (m, angle) {
angle = angle * (Math.PI / 180);
const c = Math.cos(angle),
s = Math.sin(angle);
return this.multiply(m,
[
c, 0,-s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
]
);
},
rotateZ (m, angle) {
angle = angle * (Math.PI / 180);
const c = Math.cos(angle),
s = Math.sin(angle);
return this.multiply(m,
[
c,-s, 0, 0,
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
);
}
};
////////////////////////// chaining ////////////////////////////
const mat4 = () => new Mat4();
const Mat4 = class {
constructor () {
this.m = m4.identity();
}
scale (x, y, z) {
this.m = m4.scale(this.m, x, y, z);
return this;
}
translate(x, y, z) {
this.m = m4.translate(this.m, x, y, z);
return this;
}
rotateX(a) {
this.m = m4.rotateX(this.m, a);
return this;
}
rotateY(a) {
this.m = m4.rotateY(this.m, a);
return this;
}
rotateZ(a) {
this.m = m4.rotateZ(this.m, a);
return this;
}
};
/////////////////////// Geometry ////////////////////////
const cube = (transform) => {
const p = [
v4.multiplyMatrix([ 1,  1,  1, 1], transform),
v4.multiplyMatrix([-1,  1,  1, 1], transform),
v4.multiplyMatrix([-1, -1,  1, 1], transform),
v4.multiplyMatrix([ 1, -1,  1, 1], transform),
v4.multiplyMatrix([ 1,  1, -1, 1], transform),
v4.multiplyMatrix([-1,  1, -1, 1], transform),
v4.multiplyMatrix([-1, -1, -1, 1], transform),
v4.multiplyMatrix([ 1, -1, -1, 1], transform)
];
return [
polygon.createIndexed(p, [0, 1, 2, 3]),
polygon.createIndexed(p, [7, 6, 5, 4]),
polygon.createIndexed(p, [0, 3, 7, 4]),
polygon.createIndexed(p, [0, 4, 5, 1]),
polygon.createIndexed(p, [5, 6, 2, 1]),
polygon.createIndexed(p, [3, 2, 6, 7])
];
};
const pyramid = (transform, nosplit) => {
const p = [
v4.multiplyMatrix([ 1,  0,  1, 1], transform),
v4.multiplyMatrix([-1,  0,  1, 1], transform),
v4.multiplyMatrix([-1,  0, -1, 1], transform),
v4.multiplyMatrix([ 1,  0, -1, 1], transform),
v4.multiplyMatrix([ 0,  2,  0, 1], transform)
];
return [
polygon.createIndexed(p, [0, 1, 2, 3]),
polygon.createIndexed(p, [4, 1, 0]),
polygon.createIndexed(p, [4, 0, 3]),
polygon.createIndexed(p, [4, 2, 1]),
polygon.createIndexed(p, [4, 3, 2])
];
};
const diamond = (transform, nosplit) => {
const p = [
v4.multiplyMatrix([ 1,  0,  1, 1], transform),
v4.multiplyMatrix([-1,  0,  1, 1], transform),
v4.multiplyMatrix([-1,  0, -1, 1], transform),
v4.multiplyMatrix([ 1,  0, -1, 1], transform),
v4.multiplyMatrix([ 0,  1,  0, 1], transform),
v4.multiplyMatrix([ 0, -1,  0, 1], transform)
];
return [
polygon.createIndexed(p, [4, 1, 0]),
polygon.createIndexed(p, [4, 0, 3]),
polygon.createIndexed(p, [4, 2, 1]),
polygon.createIndexed(p, [4, 3, 2]),
polygon.createIndexed(p, [0, 1, 5]),
polygon.createIndexed(p, [3, 0, 5]),
polygon.createIndexed(p, [1, 2, 5]),
polygon.createIndexed(p, [2, 3, 5])
];
};
/////////////////////////// Polygon ///////////////////////////
const polygon = {
createIndexed (p, list) {
const points = [];
for (const index of list) {
points.push(p[index]);
}
points.plane = this.definePlane(points);
return points;
},
create (p) {
p.plane = this.definePlane(p);
return p;
},
definePlane (poly) {
let sum = [0,0,0,0];
for (
var i = 0, last = poly.length - 1;
i < poly.length;
last = i, i++
) {
const A = poly[last]; // assumes counter-clockwise points order
const B = poly[i];
sum[0] += (A[1] - B[1]) * (A[2] + B[2]);
sum[1] += (A[2] - B[2]) * (A[0] + B[0]);
sum[2] += (A[0] - B[0]) * (A[1] + B[1]);
}
sum = v4.normalize(sum);
const p = poly[0];
return [
sum[0],
sum[1],
sum[2],
- (
sum[0] * p[0] +
sum[1] * p[1] +
sum[2] * p[2] +
sum[3] * p[3]
)
];
},
draw (camera, poly) {
const p = polygons.create();
for (const p0 of poly) {
if (p0.frame !== camera.frame) {
const p2 = v4.multiplyMatrix(p0, camera.viewing);
p0.frame = camera.frame;
p0.x = (p2[0] / p2[3]) *  camera.aspect;
p0.y = (p2[1] / p2[3]) * -camera.aspect;
}
}
polygons.draw(turtle, p);
},
split (poly, plane) {
const outpts = [], inpts = [];
let outp, inp, out_c = 0, in_c = 0, poly_class = HC_ON;
let ptA = poly[poly.length - 1], sideA = v4.dotProduct(ptA, plane);
for (const ptB of poly) {
const sideB = v4.dotProduct(ptB, plane);
if (sideB > EPSILON) {
if (poly_class === HC_ON) poly_class = HC_OUT;
else if (poly_class !== HC_OUT) poly_class = HC_SPANNING;
if (sideA < -EPSILON) {
const v = v4.subtract(ptB, ptA);
outpts[out_c++] = inpts[in_c++] = v4.add(ptA, v4.multiplyScalar(v, -v4.dotProduct(ptA, plane) / v4.dotProduct(v, plane)));
poly_class = HC_SPANNING;
}
outpts[out_c++] = ptB;
} else if (sideB < -EPSILON) {
if (poly_class === HC_ON) poly_class = HC_IN;
else if (poly_class !== HC_IN) poly_class = HC_SPANNING;
if (sideA > EPSILON) {
const v = v4.subtract(ptB, ptA);
outpts[out_c++] = inpts[in_c++] = v4.add(ptA, v4.multiplyScalar(v, -v4.dotProduct(ptA, plane) / v4.dotProduct(v, plane)));
poly_class = HC_SPANNING;
}
inpts[in_c++] = ptB;
} else outpts[out_c++] = inpts[in_c++] = ptB;
ptA = ptB;
sideA = sideB;
}
switch (poly_class) {
case HC_OUT:
outp = poly;
break;
case HC_IN:
inp = poly;
break;
case HC_SPANNING:
outp = this.create(outpts);
inp = this.create(inpts);
break;
}
return [outp, inp, poly_class];
}
}
////////////////////////// BSP Tree class //////////////////////////////
const BspTree = class {
constructor () {
this.node = null;
}
insert (list, keep, cur = keep) {
if (list.length === 0) return;
if (this.node) {
this.node.insert(list, keep);
} else {
if ((cur === keep) || (keep === HC_SPANNING)) {
this.node = new BspNode(list.pop());
if (list.length) this.node.insert(list, HC_SPANNING);
}
}
}
pushPoly (poly, result, keep, cur) {
if (this.node !== null) this.node.pushPoly (poly, result, keep);
else if (cur === keep) result.push(poly);
}
pushList (list, result, keep, cur) {
if (list.length === 0) return;
if (this.node !== null) result = this.node.pushList (list, result, keep);
else if (cur === keep) result.push.apply(result, list);
}
reduce  () {
if (this.node !== null) this.node.reduce();
}
draw (camera) {
if (this.node !== null) this.node.draw(camera);
}
}
////////////////////////// Bsp Node Class ////////////////////////////////
const BspNode = class {
constructor (poly) {
this.plane = poly.plane;
this.on    = [poly];
this.in    = new BspTree();
this.out   = new BspTree();
}
insert (list, keep) {
const inside = [], outside = [];
for (const poly of list) {
const [outp, inp, sgn] = polygon.split(poly, this.plane);
if (sgn === HC_ON) this.on.push(poly);
else {
if ((sgn === HC_IN) || (sgn === HC_SPANNING)) inside.push(inp);
if ((sgn === HC_OUT) || (sgn === HC_SPANNING)) outside.push(outp);
}
}
if (inside.length !== 0) this.in.insert(inside, keep, HC_IN);
if (outside.length !== 0) this.out.insert(outside, keep, HC_OUT);
}
pushPoly (poly, result, keep) {
const [outp, inp, sgn] = polygon.split(poly, this.plane);
if (sgn === HC_ON) result.push(poly);
else {
if ((sgn === HC_IN) || (sgn === HC_SPANNING)) this.in.pushPoly (inp, result, keep, HC_IN);
if ((sgn === HC_OUT) || (sgn === HC_SPANNING)) this.out.pushPoly (outp, result, keep, HC_OUT);
}
}
pushList (list, result, keep) {
const inside = [], outside = [];
for (const poly of list) {
const [outp, inp, sgn] = polygon.split(poly, this.plane);
if (sgn === HC_ON) result.push(poly);
else {
if ((sgn === HC_IN) || (sgn === HC_SPANNING)) inside.push(inp);
if ((sgn === HC_OUT) || (sgn === HC_SPANNING)) outside.push(outp);
}
}
if (inside.length !== 0) this.in.pushList (inside, result, keep, HC_IN);
if (outside.length !== 0) this.out.pushList (outside, result, keep, HC_OUT);
}
reduce () {
const results = [], boundary = [];
for (const poly of this.on) {
if (Math.abs(poly.plane[3] + this.plane[3]) > EPSILON) {
this.in.pushPoly (poly, results, HC_IN, HC_IN);
this.out.pushList (results, boundary, HC_OUT, HC_OUT);
} else {
this.out.pushPoly (poly, results, HC_OUT, HC_OUT);
this.in.pushList (results, boundary, HC_IN, HC_IN);
}
}
this.on = boundary;
this.in.reduce();
this.out.reduce();
}
// front to back tree traversal
draw  (camera) {
var sgn = v4.dotProduct(camera.eye, this.plane);
if (sgn >= 0) {
this.out.draw(camera);
for (const on of this.on) polygon.draw(camera, on);
this.in.draw(camera);
} else {
this.in.draw(camera);
this.out.draw(camera);
}
}
}
////////////////// camera /////////////////////
const camera = {
frame: 1,
aspect: 100,
drawScene (world, rx, ry, rz) {
this.frame++;
const transformation = m4.rotateXYZ(rx, ry, rz);
this.viewing = m4.multiply(transformation, this.view);
this.eye = v4.multiplyMatrix(this.posEye, m4.inverse(transformation));
world.draw(camera);
},
look (e, to, zoom) {
this.posEye = [e[0],e[1],e[2],1];
const vpn = v4.normalize(v4.subtract(this.posEye, [to[0],to[1],to[2],1]));
const u = v4.crossProduct([0, 1, 0], vpn);
const v = v4.crossProduct(vpn, u);
const vrp = v4.add(this.posEye, v4.multiplyScalar(vpn, -zoom));
this.view = m4.multiply(this.viewMatrix(u, v, vpn, vrp), this.perspective(zoom));
},
perspective (distance) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1,-1 / distance,
0, 0, 0, 1
];
},
viewMatrix (u, v, n, r) {
return [
u[0], v[0], n[0], 0,
u[1], v[1], n[1], 0,
u[2], v[2], n[2], 0,
-(r[0] * u[0] + r[1] * u[1] + r[2] * u[2] + r[3] * u[3]),
-(r[0] * v[0] + r[1] * v[1] + r[2] * v[2] + r[3] * v[3]),
-(r[0] * n[0] + r[1] * n[1] + r[2] * n[2] + r[3] * n[3]), 1
];
}
}

////////////////////////////////////////////////////////////////////////////////

const HC_IN = -1, HC_ON = 0, HC_OUT = 1, HC_SPANNING = 2, INFINITY = 100000, EPSILON  = 1 / 100000;

const world = new BspTree();
world.insert(cube(mat4().scale(-5,-2.5,-5).m), 1);// outside walls
world.insert(cube(mat4().m), 1); // insert a basic cube
world.insert(cube(mat4().scale(-1.5,-0.875,-1.5).translate(0.625,0,0).m), -1); // cut out the cube to make a big "C"
world.insert(cube(mat4().scale(0.15,0.4,0.4).translate(-0.95,0,0).rotateX(45).m), 1); // window frame
world.insert(cube(mat4().scale(-0.3,-0.3,-0.3).translate(-1,0,0).rotateX(45).m), -1); // window hole
world.insert(cube(mat4().scale(0.15,0.4,0.4).translate(0.8,0,0).rotateX(45).m), 1); // window frame
world.insert(cube(mat4().scale(0.0625,1.8,0.0625).translate(0.8,0,0).m), 1); // support
world.insert(cube(mat4().scale(-1.5,-0.3,-0.3).translate(0.8,0,0).rotateX(45).m), -1); // window hole
world.insert(diamond(mat4().rotateZ(90).scale(4,0.15,0.15).rotateX(45).m), 1); // long diamond
world.insert(pyramid(mat4().scale(0.5,0.5,0.5).translate(0,-1.25,0).rotateZ(180).m), 1); // pyramid
world.insert(pyramid(mat4().scale(0.5,0.5,0.5).translate(0,-1.25,0).m), 1); // pyramid
world.insert(cube(mat4().scale(0.5,0.05,0.5).translate(0,-1.4,0).m), 1);// slab
world.insert(cube(mat4().scale(0.51,0.05,0.51).translate(0,-1.6,0).m), 1); // slab
world.insert(cube(mat4().scale(0.5,0.05,0.5).translate(0,1.4,0).m), 1); // slab
world.insert(cube(mat4().scale(0.51,0.05,0.51).translate(0,1.6,0).m), 1); // slab
world.reduce(); // extrude negative parts
camera.look([0,0,8], [0,0,0], 3);
camera.drawScene(world, 0.2, Math.random() * 2 * Math.PI, 0);

////////////////////////////////////////////////////////////////
// reinder's occlusion code parts from "Cubic space division #2"
// Optimizations and code clean-up by ge1doot
////////////////////////////////////////////////////////////////

function Polygons() {
const polygonList = [];
const Polygon = class {
constructor() {
this.cp = [];       // clip path: array of [x,y] pairs
this.dp = [];       // 2d line to draw
this.aabb = [];     // AABB bounding box
}
for (let i = 0; i < points.length; i++) this.cp.push(points[i]);
this.aabb = this.AABB();
}
for (let i = s, l = this.cp.length; i < l; i++) {
this.dp.push(this.cp[i], this.cp[(i + 1) % l]);
}
}
draw(t) {
if (this.dp.length === 0) return;
for (let i = 0, l = this.dp.length; i < l; i+=2) {
const d0 = this.dp[i];
const d1 = this.dp[i + 1];
t.penup();
t.goto(d0);
t.pendown();
t.goto(d1);
}
}
AABB() {
let xmin = 2000;
let xmax = -2000;
let ymin = 2000;
let ymax = -2000;
for (let i = 0, l = this.cp.length; i < l; i++) {
const x = this.cp[i][0];
const y = this.cp[i][1];
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
// Bounding box: center x, center y, half w, half h
return [
(xmin + xmax) * 0.5,
(ymin + ymax) * 0.5,
(xmax - xmin) * 0.5,
(ymax - ymin) * 0.5
];
}
const tp = new Polygon();
tp.cp.push(
[this.aabb[0] - this.aabb[2], this.aabb[1] - this.aabb[3]],
[this.aabb[0] + this.aabb[2], this.aabb[1] - this.aabb[3]],
[this.aabb[0] + this.aabb[2], this.aabb[1] + this.aabb[3]],
[this.aabb[0] - this.aabb[2], this.aabb[1] + this.aabb[3]]
);
const dx = Math.sin(a) * d, dy = Math.cos(a) * d;
const cx = Math.sin(a) * 200, cy = Math.cos(a) * 200;
for (let i = 0.5; i < 150 / d; i++) {
tp.dp.push([dx * i + cy, dy * i - cx], [dx * i - cy, dy * i + cx]);
tp.dp.push([-dx * i + cy, -dy * i - cx], [-dx * i - cy, -dy * i + cx]);
}
tp.boolean(this, false);
for (let i = 0, l = tp.dp.length; i < l; i++) this.dp.push(tp.dp[i]);
}
inside(p) {
// find number of i ntersection points from p to far away
const p1 = [0.1, -1000];
let int = 0;
for (let i = 0, l = this.cp.length; i < l; i++) {
if ( (p[0]-this.cp[i][0])**2 +  (p[1]-this.cp[i][1])**2 <= 0.001) return false;
if (
this.vec2_find_segment_intersect(
p,
p1,
this.cp[i],
this.cp[(i + 1) % l]
) !== false
) {
int++;
}
}
return int & 1;
}
boolean(p, diff = true) {
// polygon diff algorithm
const ndp = [];
for (let i = 0, l = this.dp.length; i < l; i+=2) {
const ls0 = this.dp[i];
const ls1 = this.dp[i + 1];
// find all intersections with clip path
const int = [];
for (let j = 0, cl = p.cp.length; j < cl; j++) {
const pint = this.vec2_find_segment_intersect(
ls0,
ls1,
p.cp[j],
p.cp[(j + 1) % cl]
);
if (pint !== false) {
int.push(pint);
}
}
if (int.length === 0) {
// 0 intersections, inside or outside?
if (diff === !p.inside(ls0)) {
ndp.push(ls0, ls1);
}
} else {
int.push(ls0, ls1);
// order intersection points on line ls.p1 to ls.p2
const cmpx = ls1[0] - ls0[0];
const cmpy = ls1[1] - ls0[1];
for (let i = 0, len = int.length; i < len; i++) {
let j = i;
const item = int[j];
for (
const db = (item[0] - ls0[0]) * cmpx + (item[1] - ls0[1]) * cmpy;
j > 0 && (int[j - 1][0] - ls0[0]) * cmpx + (int[j - 1][1] - ls0[1]) * cmpy < db;
j--
) int[j] = int[j - 1];
int[j] = item;
}
for (let j = 0; j < int.length - 1; j++) {
if (
(int[j][0] - int[j + 1][0]) ** 2 + (int[j][1] - int[j + 1][1]) ** 2 >= 0.01
) {
if (
diff ===
!p.inside([
(int[j][0] + int[j + 1][0]) / 2,
(int[j][1] + int[j + 1][1]) / 2
])
) {
ndp.push(int[j], int[j + 1]);
}
}
}
}
}
this.dp = ndp;
return this.dp.length > 0;
}
//port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs
vec2_find_segment_intersect(l1p1, l1p2, l2p1, l2p2) {
const d = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]);
if (d === 0) return false;
const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]);
const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]);
const ua = n_a / d;
const ub = n_b / d;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return [
l1p1[0] + ua * (l1p2[0] - l1p1[0]),
l1p1[1] + ua * (l1p2[1] - l1p1[1])
];
}
return false;
}
};
return {
create() {
return new Polygon();
},
draw(t, p) {
let vis = true;
for (let j = 0; j < polygonList.length; j++) {
const p1 = polygonList[j];
// AABB overlapping test - still O(N2) but very fast
if (
Math.abs(p1.aabb[0] - p.aabb[0]) - (p.aabb[2] + p1.aabb[2]) < 0 &&
Math.abs(p1.aabb[1] - p.aabb[1]) - (p.aabb[3] + p1.aabb[3]) < 0
) {
if (p.boolean(p1) === false) {
vis = false;
break;
}
}
}
if (vis) {
p.draw(t);
polygonList.push(p);
}
}
};
}```