(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-polygon'), require('d3-array'), require('earcut')) : typeof define === 'function' && define.amd ? define(['exports', 'd3-polygon', 'd3-array', 'earcut'], factory) : (factory((global.scissors = global.scissors || {}),global.d3,global.d3,global.earcut)); }(this, function (exports,d3Polygon,d3Array,earcut) { 'use strict'; earcut = 'default' in earcut ? earcut['default'] : earcut; // Computes the dot product between 2 vectors. function dot(a, b) { var res = 0.0, i; if (a.length != b.length){ throw new Error("Invalid dimensions for dot product."); } for (i = 0; i < a.length; i++) { res += a[i] * b[i]; } return res; } function length(u) { return Math.sqrt(dot(u, u)); } function scale(s, a) { return [s * a[0], s * a[1]]; } function normalize(a) { return scale(1 / length(a), a); } // Returns angle in radians between AB and AC, where a, b, c // are specified in counter-clockwise order. function angle(a, b, c) { var u = normalize([b[0] - a[0], b[1] - a[1]]), v = normalize([c[0] - a[0], c[1] - a[1]]); if ((length(u) * length(v)) == 0) return 0; return Math.acos(dot(u, v)); } // Returns a vector pointing along the z-axis with length ||AB x AC||. function cross(a, b, c) { var u, v, z, res; u = normalize([b[0] - a[0], b[1] - a[1]]), v = normalize([c[0] - a[0], c[1] - a[1]]), z = u[0] * v[1] - u[1] * v[0], res = [0, 0, 0]; res[0] = res[1] = 0; res[2] = z; return res; } // Tests if point is to the right of an edge (CCW order). function right(point, edge) { return cross(point, edge[0], edge[1])[2] < 0; } // Sort points in-place to produce counterclockwise winding order. function ccw(P) { var C = P.centroid(); if (!right(C, P.slice(0, 2))) P.reverse(); return P; } function pairwise(a, b, operation){ var res = [], i; for(i = 0; i < a.length; i++){ res[i] = operation(a[i], b[i]); } return res; } function add(a, b) { return pairwise(a, b, function(x, y){ return x + y; }); } function sub(a, b) { return pairwise(a, b, function(x, y){ return x - y; }); } var infinity = [1e12, 1e12]; // Returns point of intersection of AB and CD, // possibly occuring at infinity. function infiniteIntersect(a, b, c, d) { var u = sub(b, a), v = sub(d, c), q = sub(c, a), denom, s; // Cramer's Rule denom = u[0] * (-v[1]) - (-v[0]) * u[1]; // Check for parallel lines if (denom === 0) return [infinity, infinity]; s = (q[0] * (-v[1]) - (-v[0]) * q[1]) / (denom); return add(a, scale(s, u)); } function identity(n) { var i, j, eye = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; for (i = 0; i < n; i++){ for (j = 0; j < n; j++){ eye[i][j] = 0.0; } } for (i = 0; i < n; i++){ eye[i][i] = 1.0; } return eye; } function row(a, row) { var res = [], i; for (i = 0; i < a[0].length; i++){ res[i] = a[row][i]; } return res; } // Multiplies matrix A by vector b and returns the resulting vector. function multiply(A, b) { var res = [], i, x = b.slice(); // Pad for homogenous coordinates. for (i = 0; i < 3 - b.length; i++) { x.push(1); } for(i = 0; i < A.length; i++){ res[i] = dot(row(A, i), x); } return res; } // Returns column col from matrix a. function column(a, col) { var res = [], i; for (i = 0; i < a.length; i++){ res[i] = a[i][col]; } return res; } // Multiplies matrix A by matrix B and returns the resulting matrix. function Multiply(A, B) { var i, j, res = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; if(A[0].length != B.length){ throw new Error("invalid dimensions"); } for(i = 0; i < A.length; i++){ for(j = 0; j < B[0].length; j++){ res[i][j] = dot(row(A, i), column(B, j)); } } return res; } function rotation(theta) { var rad = theta * Math.PI / 180.0; var s = Math.sin(rad); var c = Math.cos(rad); return [[c, -1.0 * s, 0], [s, c, 0], [0, 0, 1]]; } function translation(delta) { var dx = delta[0]; var dy = delta[1]; var res = identity(3); res[0][2] = dx; res[1][2] = dy; return res; } // Polygon constructor function Polygon() { this.transforms = []; this._matrix = identity(3); this._origin = null; } Polygon.prototype = Object.create(Array.prototype); // subclass Array Polygon.prototype.constructor = Polygon; Polygon.prototype.centroid = centroid; Polygon.prototype.containsPoint = containsPoint; Polygon.prototype.translate = translate; Polygon.prototype.rotate = rotate; Polygon.prototype.scale = resize; Polygon.prototype.accumulate = accumulate; Polygon.prototype.origin = origin; Polygon.prototype.clone = clone; function centroid() { return d3Polygon.polygonCentroid(this); } function containsPoint(point){ return d3Polygon.polygonContains(this, point); } function translate(T) { var i, v, n; for (i = 0, n = this.length; i < n; i++) { v = this[i]; this[i] = [v[0] + T[0], v[1] + T[1]]; } return this; } function rotate(theta, pivot) { var R = rotation(theta), i, n; if (pivot) { this.translate([-pivot[0], -pivot[1]]); } for (i = 0, n = this.length; i < n; i++) { this[i] = multiply(R, this[i]); } if (pivot) { this.translate(pivot); } return this; } function resize(factor) { var i, n; for (i = 0, n = this.length; i < n; i++) { this[i] = scale(factor, this[i]); } return this; } // Returns a clone of this polygon with transform history applied. function origin() { if (this._origin) return this._origin; this._origin = this.accumulate(); return this._origin; } // Apply transform history to a clone of polygon. function accumulate() { var P = this.clone(), n = this.transforms.length, M = identity(3), i, transform; // Most recent transforms are pushed to end of array. for (i = n - 1; i >= 0; i--) { transform = this.transforms[i]; if (transform.rotate && transform.pivot) { // pivot required P.rotate(transform.rotate, transform.pivot); M = Multiply(translation(scale(-1, transform.pivot)), M); M = Multiply(rotation(transform.rotate), M); M = Multiply(translation(transform.pivot), M); } else if (transform.translate) { P.translate(transform.translate); M = Multiply(translation(transform.translate), M); } } P._matrix = M; return P; } // Produces deep copy of vertex positions, shallow copy of other attributes function clone() { var positions = JSON.parse(JSON.stringify(this.slice(0))); return Object.assign(polygon(positions), this); } // Create new Polygon from array of position tuples. function polygon(positions) { var P = new Polygon(); P.push.apply(P, positions); return P; } // Returns the undo operation for a translation or rotation about a pivot. function undo(transform){ var undone; if (transform.rotate && transform.pivot) { undone = {rotate: -transform.rotate, pivot: transform.pivot}; } else if (transform.translate) { undone = {translate: scale(-1, transform.translate)}; } return undone; } // Sutherland-Hodgeman algorithm for convex subject and clip polygons. function clipPolygon(subject, clip){ var inputList, outputList, clipped, i, j, n, end, edge, E, S, // Two subject vertices. eInside, sInside, intersection, undone, transforms; // Ensure clip polygon is traversed in clockwise order. clip = ccw(clip.clone()).reverse(); ccw(subject); outputList = subject.slice(0); for (i = 0, n = clip.length; i < n; i++) { end = (i + 1) % n; edge = [clip[i], clip[end]]; inputList = outputList.slice(0); outputList = []; S = inputList[inputList.length - 1]; for (j = 0; j < inputList.length; j++) { E = inputList[j]; eInside = !right(E, edge); sInside = !right(S, edge); if (eInside){ if (!sInside){ // exit clip region intersection = infiniteIntersect(S, E, clip[i], clip[end]); outputList.push(intersection); } outputList.push(E); } else if (sInside) { // enter clip region intersection = infiniteIntersect(S, E, clip[i], clip[end]); outputList.push(intersection); } S = E; } } clipped = polygon(outputList); // Subject polygon's original transform history. transforms = subject.transforms.slice(0); // Append reversed (and undone) transfer history of clip polygon. clipped.target = clip.transforms.slice(0); undone = clip.transforms.slice(0).reverse(0).map(undo); transforms = transforms.concat(undone); clipped.transforms = transforms; return clipped; } // Takes in arrays of polygons and returns intersection between the // collections using the Sutherland-Hodgeman algorithm. function clipCollection(A, B){ var result = [], clipped, i, j; // Clip every polygon in A against every polygon in B. for (i = 0; i < A.length; i++) { for (j = 0; j < B.length; j++) { clipped = clipPolygon(ccw(A[i]), ccw(B[j])); if (clipped.length > 2) result.push(clipped); } } return result; } function degree(rad) { return rad * 180 / Math.PI; } function inDelta(actual, expected, epsilon) { return actual < expected + epsilon && actual > expected - epsilon; } // Returns point of intersection of AB and CD // or null if segments do not intersect. function intersect(a, b, c, d) { var u = sub(b, a), v = sub(d, c), q = sub(c, a), denom, s, t, epsilon = 0.0; // Cramer's Rule denom = u[0] * (-v[1]) - (-v[0]) * u[1]; s = (q[0] * (-v[1]) - (-v[0]) * q[1]) / (denom); t = (u[0] * q[1] - q[0] * u[1]) / (denom); return (inDelta(s, 0.5, 0.5 + epsilon) && inDelta(t, 0.5, 0.5 + epsilon)) ? add(a, scale(s, u)) : null; } // Returns array of polygons resulting from cutting // convex polygon P by segment ab. If the polygon is // not cut, an empty array is returned. function cutPolygon(a, b, P) { var n = P.length, _P = [], P_ = [], points = [], e, f = P[n - 1], bounds = [], i = -1, polygons = [], x; // walk through polygon edges, attempting intersections // with exact vertices or line segments while (++i < n) { e = f; f = P[i]; // compare floating-point positions by reference if ((a === e || b === e) && !(points.length && Object.is(points[0], e))) x = e; else if ((a === f || b === f) && !(points.length && !Object.is(points[0], f))) x = f; else x = intersect(a, b, e, f); if (!x) continue; // no intersection if (points.length == 2) break; points.push(x); bounds.push(i); } // stitch together two generated polygons if (points.length == 2 && !Object.is(points[0], points[1])) { // stitch half of polygon _P.push(points[0]); i = bounds[0]; while (i != bounds[1]) { // check point is not an EXACT intersection if (!Object.is(points[0], P[i]) && ! Object.is(points[1], P[i])) _P.push(P[i]); i = (i + 1) % n; } _P.push(points[1]); // stitch other half of polygon P_.push(points[0]); P_.push(points[1]); i = bounds[1]; while (i != bounds[0]) { // check point is not an EXACT intersection if (!Object.is(points[0], P[i]) && ! Object.is(points[1], P[i])) P_.push(P[i]); i = (i + 1) % n; } polygons.push(polygon(_P), polygon(P_)); // transfer parent properties polygons.forEach(function(d) { [].push.apply(d.transforms, P.transforms); }); } return polygons; } function cutCollection(a, b, collection) { var N = collection.length, indices = [], polygons = [], P, generated, i; for (i = 0; i < N; i++) { P = collection[i]; generated = cutPolygon(a, b, P); if (generated.length == 2) { [].push.apply(polygons, generated); indices.push(i); } } // include polygons that were not cut into two new polygons polygons = polygons.concat(collection.filter(function(d, i) { return indices.indexOf(i) === -1; })); return polygons; } // Takes in a collection of polygons forming a rectangle and produces // a new collection forming a rectangle of the given width. function rectangle2Rectangle(collection, width) { var A, B, C, D, E, F, G, J, K, // follows Tralie's labeling AFGD, KCF, area, height, rectangle = collection.rectangle, // bounding rectangle l = Infinity, polygons = [], i, n, a, b, left, halved, slideLeft, slideUp, E_Polygon, E_Vertex, F_Polygon, F_Vertex, centroid; area = Math.abs(d3Polygon.polygonArea(rectangle)); height = area / width; // square side length collection = collection.map(function(d) { return d.clone(); }); // Bounding rectangle for incoming collection. B = rectangle[0]; F = rectangle[1]; G = rectangle[2]; E = rectangle[3]; l = length(sub(B, F)); if (width > l) { width = height; height = area / width; } // The rectangle to be produced, defined by [A, B, C, D]. A = add(B, scale(height, normalize(sub(E, B)))); C = add(B, scale(width, normalize(sub(F, B)))); D = add(A, sub(C, B)); // halving the canonical rectangle for the escalator method while (l > 2 * width) { E_Polygon = [], E_Vertex = [], F_Polygon = [], F_Vertex = []; a = add(A, scale(0.5, sub(F, B))); b = add(B, scale(0.5, sub(F, B))); l = length(sub(b, F)); left = [A, B, b, a]; // "overshoot" cut segment endpoints to ensure intersection halved = cutCollection(a, add(b, sub(C, D)), collection); slideUp = sub(E, B); slideLeft = sub(B, b); // translate polgons resulting from the cut (i.e., "stacking the two halves") halved.forEach(function(d, j) { centroid = d3Polygon.polygonCentroid(d); // update exact references to vertices used in exact intersections d.forEach(function(V, k) { // scan vertices if (Object.is(E, V)) { E_Polygon.push(j); E_Vertex.push(k); } else if (Object.is(F, V)) { F_Polygon.push(j); F_Vertex.push(k); } }); if (d3Polygon.polygonContains(left, centroid)) { // slide "up" d.translate(slideUp).transforms.push({translate: scale(-1, slideUp)}); // undo translate } else { // slide "left" d.translate(slideLeft).transforms.push({translate: scale(-1, slideLeft)}); // undo translate } }); E = halved[E_Polygon[0]][E_Vertex[0]]; F = halved[F_Polygon[0]][F_Vertex[0]]; // Make all vertices at F share the same reference. for (i = 1, n = F_Vertex.length; i < n; i++) { F = halved[F_Polygon[i]][F_Vertex[i]] = halved[F_Polygon[i - 1]][F_Vertex[i - 1]]; } collection = halved; } polygons = collection; // The "elevator method" assumes rectangle length < (2 x square height). G = add(F, sub(E, B)); J = intersect(A, F, E, G); K = intersect(A, F, D, C); KCF = [K, C, F]; // used to locate elevator pieces AFGD = [A, F, G, D]; polygons = cutCollection(A, F, polygons); polygons = cutCollection(D, add(C, scale(1, sub(C, D))), polygons); // Slide new polygons using elevator method. polygons.forEach(function(d) { var centroid, T; centroid = d3Polygon.polygonCentroid(d); if (d3Polygon.polygonContains(KCF, centroid)) { T = sub(A, K); d.translate(T).transforms.push({translate: scale(-1, T)}); } else if (d3Polygon.polygonContains(AFGD, centroid)) { T = sub(A, J); d.translate(T).transforms.push({translate: scale(-1, T)}); } }); polygons.rectangle = (isWide([B, C, D, A])) ? [B, C, D, A] : [C, D, A, B]; return polygons; } // Assumes BCDA orientation, where a rectangle is wide if // side BC is longer than BA. function isWide(rectangle) { var A = rectangle[3], B = rectangle[0], C = rectangle[1]; return length(sub(B, C)) > length(sub(B, A)); } // Takes in a list of polygon collections, each arranged in a rectangle // of equal width and returns a list of polygon collections that have been // arranged to fit in a square. // The returned list of collections has property `square` which // denotes the bounding vertices for the stacked collections. function stack(boxes) { var previous, current, A, B, C, D, pivot, snap, stacked = [], i, n, T, direction, theta; if (boxes.length < 2) { stacked = boxes.slice(); stacked.square = stacked[0].rectangle; return stacked; } previous = boxes[0]; stacked.push(previous); for (i = 1, n = boxes.length; i < n; i++) { pivot = previous.rectangle[3]; current = boxes[i]; snap = current.rectangle[0]; T = sub(pivot, snap); current.rectangle = polygon(current.rectangle).translate(T); direction = cross(pivot, previous.rectangle[2], current.rectangle[1])[2] > 0 ? -1 : 1; theta = degree(angle(pivot, previous.rectangle[2], current.rectangle[1])); theta *= direction; // Translate collection of polygons forming a rectangle of a given width. // Align a vertex and pivot rectangle to fit flush with previous collection. current.forEach(function(d) { d.translate(T); d.transforms.push({ translate: scale(-1, T) }); d.rotate(theta, pivot); d.transforms.push({ rotate: -theta, pivot: pivot, }); }); current.rectangle.rotate(theta, pivot); stacked.push(current); previous = current; } A = stacked[n - 1].rectangle[3]; B = stacked[0].rectangle[0]; C = stacked[0].rectangle[1]; D = stacked[n - 1].rectangle[2]; stacked.square = [B, C, D, A]; return stacked; } function project(u, v) { return scale(dot(u, v) / dot(v, v), v); } function triangle2Rectangle(P) { var index = 0, A, B, C, D, E, F, G, H, M, BC, BA, MB, MC, ME, BCFD, DGB, FCH, maxAngle = -Infinity, polygons, theta, i; if (d3Polygon.polygonArea(P) == 0) return []; // Find largest angle in triangle T. for (i = 0; i < 3; i++) { theta = 0; theta = angle(P[i % 3], P[(i + 1) % 3], P[(i + 2) % 3]); if (theta > maxAngle) { maxAngle = theta; index = i; } } A = P[index]; B = P[(index + 1) % 3]; C = P[(index + 2) % 3]; BC = sub(C, B); BA = sub(A, B); M = add(B, project(BA, BC)); E = add(A, scale(0.5, sub(M, A))); MB = sub(B, M); MC = sub(C, M); ME = sub(E, M); G = add(M, add(MB, ME)); H = add(M, add(MC, ME)); D = intersect(E, G, A, B); // pivot F = intersect(E, H, A, C); // pivot BCFD = polygon([B, C, F, D]); DGB = polygon([D, G, B]); FCH = polygon([F, C, H]); // transforms to return polygons to previous positions DGB.transforms.push({rotate: degree(Math.PI), pivot: D}); FCH.transforms.push({rotate: degree(-Math.PI), pivot: F}); polygons = [BCFD, DGB, FCH]; polygons.rectangle = [B, C, H, G]; return polygons; } // Takes in source and subject meshes, returns a decomposition layout. function decompose(source, subject) { var A, B, sourceStack, subjectStack, squareA, squareB, centroid, target, T, clipped, theta, direction, i = 0, j = 0, distance, min = Infinity, factor, area, width; source = source.map(function(d) { return polygon(d); }); subject = subject.map(function(d) { return polygon(d); }); area = collectionArea(source); width = Math.sqrt(area); factor = Math.sqrt(area / collectionArea(subject) ); scaleCollection(factor, subject); sourceStack = stack(source.map(function(d) {return stackable(width, d); })); subjectStack = stack(subject.map(function(d) {return stackable(width, d); })); A = flatten(sourceStack); B = flatten(subjectStack); A.square = sourceStack.square; B.square = subjectStack.square; squareA = polygon(A.square); squareB = polygon(B.square); centroid = squareA.centroid(); target = squareB.centroid(); T = [(centroid[0] - target[0]), (centroid[1] - target[1])]; squareB = squareB.clone().translate(T); // find nearest vertex coorespondence in Q for first vertex in P for (i = 0, j = 0; i < 4; i++) { distance = length(sub(squareA[0], squareB[i])); if (distance < min) { min = distance; j = i; } } // rotate through a minimal angle, in the correct direction direction = cross(centroid, squareB[j], squareA[0])[2] > 0 ? 1 : -1; theta = direction * degree(angle(centroid, squareB[j], squareA[0])); // center collection B on collection A B.forEach(function(d) { d.translate(T); d.transforms.push({ translate: scale(-1, T), }); d.rotate(theta, centroid); d.transforms.push({ rotate: -theta, pivot: centroid, }); }); clipped = clipCollection(A, B); clipped = clipped.map(function(d) { var p = d.clone(); p.transforms = d.target; p = p.origin(); // place in subject triangle p.transforms = d.transforms; // restore joined transform history return p; }); clipped.scale = factor; return clipped; } function stackable(width, triangle) { return rectangle2Rectangle(triangle2Rectangle(triangle), width); } function flatten(groups) { var flattened = []; groups.forEach(function(d) { [].push.apply(flattened, d); }); return flattened; } function collectionCentroid(collection) { var centroids; centroids = collection.map(function(d) { return polygon(d).centroid(); }); if (centroids.length == 1) { return centroids[0]; } if (centroids.length == 2) { return [(centroids[0][0] + centroids[1][0]) / 2, (centroids[0][1] + centroids[1][1]) / 2]; } return polygon(centroids).centroid(); } function collectionArea(collection) { return d3Array.sum(collection, function(d) { return d3Polygon.polygonArea(d); }); } function scaleCollection(factor, collection) { var C = collectionCentroid(collection); collection.forEach(function(d) { d.translate(scale(-1, C)); d.scale(factor); d.translate(C); }); return collection; } function decomposition(source, subject) { var polygons = decompose(source, subject); return { source: function() { return polygons.map(function(d) { return d.origin().slice(0); }); }, subject: function() { return polygons.map(function(d) { return d.slice(0); }); }, scale: function() { return polygons.scale; } } } // External Mapbox earcut module. function triangulate(vertices) { var indices, triangles; indices = earcut(flattenCoordinates(vertices)); triangles = unflattenTriangles(decodeIndices(indices, vertices)); return triangles; } function flattenCoordinates(points) { var i, n, flattened = []; for (i = 0, n = points.length; i < n; i++) { flattened.push(points[i][0], points[i][1]); } return flattened; } function decodeIndices(indices, points) { return indices.map(function(i) { return points[i]; }); } function unflattenTriangles(points) { var i, n, decoded = []; for (i = 0, n = points.length; i < n; i += 3) { decoded.push([points[i], points[i + 1], points[i + 2]]); } return decoded; } function equidecompose(source, subject) { var sourceTriangles = orient(triangulate(source)), subjectTriangles = orient(triangulate(subject)); return decomposition(sourceTriangles, subjectTriangles); } // In-place reversal of clockwise winding produced by earcut. function orient(triangles) { return triangles.map(function(d) { return d.reverse(); }); } function equidecomposeMesh(source, subject) { return decomposition(source, subject); } exports.equidecompose = equidecompose; exports.equidecomposeMesh = equidecomposeMesh; exports.triangle2Rectangle = triangle2Rectangle; exports.rectangle2Rectangle = rectangle2Rectangle; }));