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
Log in to post a comment.
// 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 * Copyright (c)1994-97 Bretton Wade * 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] ]; }, add (v, p) { 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; } p.addPoints([p0.x, p0.y]); } p.addOutline(0); 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 } addPoints(...points) { for (let i = 0; i < points.length; i++) this.cp.push(points[i]); this.aabb = this.AABB(); } addOutline(s = 0) { 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 ]; } addHatching(a, d) { 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); } } }; }