import { fromPoints } from '../core/bbox';
|
import BoundingRect from '../core/BoundingRect';
|
import Point from '../core/Point';
|
import { map } from '../core/util';
|
import Path from '../graphic/Path';
|
import Polygon from '../graphic/shape/Polygon';
|
import Rect from '../graphic/shape/Rect';
|
import Sector from '../graphic/shape/Sector';
|
import { pathToPolygons } from './convertPath';
|
import { clonePath } from './path';
|
|
// Default shape dividers
|
// TODO divide polygon by grids.
|
interface BinaryDivide {
|
(shape: Path['shape']): Path['shape'][]
|
}
|
|
/**
|
* Calculating a grid to divide the shape.
|
*/
|
function getDividingGrids(dimSize: number[], rowDim: number, count: number) {
|
const rowSize = dimSize[rowDim];
|
const columnSize = dimSize[1 - rowDim];
|
|
const ratio = Math.abs(rowSize / columnSize);
|
let rowCount = Math.ceil(Math.sqrt(ratio * count));
|
let columnCount = Math.floor(count / rowCount);
|
if (columnCount === 0) {
|
columnCount = 1;
|
rowCount = count;
|
}
|
|
const grids: number[] = [];
|
for (let i = 0; i < rowCount; i++) {
|
grids.push(columnCount);
|
}
|
const currentCount = rowCount * columnCount;
|
// Distribute the remaind grid evenly on each row.
|
const remained = count - currentCount;
|
if (remained > 0) {
|
// const stride = Math.max(Math.floor(rowCount / remained), 1);
|
for (let i = 0; i < remained; i++) {
|
grids[i % rowCount] += 1;
|
}
|
}
|
return grids;
|
}
|
|
|
// TODO cornerRadius
|
function divideSector(sectorShape: Sector['shape'], count: number, outShapes: Sector['shape'][]) {
|
const r0 = sectorShape.r0;
|
const r = sectorShape.r;
|
const startAngle = sectorShape.startAngle;
|
const endAngle = sectorShape.endAngle;
|
const angle = Math.abs(endAngle - startAngle);
|
const arcLen = angle * r;
|
const deltaR = r - r0;
|
|
const isAngleRow = arcLen > Math.abs(deltaR);
|
const grids = getDividingGrids([arcLen, deltaR], isAngleRow ? 0 : 1, count);
|
|
const rowSize = (isAngleRow ? angle : deltaR) / grids.length;
|
|
for (let row = 0; row < grids.length; row++) {
|
const columnSize = (isAngleRow ? deltaR : angle) / grids[row];
|
for (let column = 0; column < grids[row]; column++) {
|
const newShape = {} as Sector['shape'];
|
|
if (isAngleRow) {
|
newShape.startAngle = startAngle + rowSize * row;
|
newShape.endAngle = startAngle + rowSize * (row + 1);
|
newShape.r0 = r0 + columnSize * column;
|
newShape.r = r0 + columnSize * (column + 1);
|
}
|
else {
|
newShape.startAngle = startAngle + columnSize * column;
|
newShape.endAngle = startAngle + columnSize * (column + 1);
|
newShape.r0 = r0 + rowSize * row;
|
newShape.r = r0 + rowSize * (row + 1);
|
}
|
|
newShape.clockwise = sectorShape.clockwise;
|
newShape.cx = sectorShape.cx;
|
newShape.cy = sectorShape.cy;
|
|
outShapes.push(newShape);
|
}
|
}
|
}
|
|
function divideRect(rectShape: Rect['shape'], count: number, outShapes: Rect['shape'][]) {
|
const width = rectShape.width;
|
const height = rectShape.height;
|
|
const isHorizontalRow = width > height;
|
const grids = getDividingGrids([width, height], isHorizontalRow ? 0 : 1, count);
|
const rowSizeDim = isHorizontalRow ? 'width' : 'height';
|
const columnSizeDim = isHorizontalRow ? 'height' : 'width';
|
const rowDim = isHorizontalRow ? 'x' : 'y';
|
const columnDim = isHorizontalRow ? 'y' : 'x';
|
const rowSize = rectShape[rowSizeDim] / grids.length;
|
|
for (let row = 0; row < grids.length; row++) {
|
const columnSize = rectShape[columnSizeDim] / grids[row];
|
for (let column = 0; column < grids[row]; column++) {
|
const newShape = {} as Rect['shape'];
|
newShape[rowDim] = row * rowSize;
|
newShape[columnDim] = column * columnSize;
|
newShape[rowSizeDim] = rowSize;
|
newShape[columnSizeDim] = columnSize;
|
|
newShape.x += rectShape.x;
|
newShape.y += rectShape.y;
|
|
outShapes.push(newShape);
|
}
|
}
|
}
|
|
function crossProduct2d(x1: number, y1: number, x2: number, y2: number) {
|
return x1 * y2 - x2 * y1;
|
}
|
|
function lineLineIntersect(
|
a1x: number, a1y: number, a2x: number, a2y: number, // p1
|
b1x: number, b1y: number, b2x: number, b2y: number // p2
|
): Point {
|
const mx = a2x - a1x;
|
const my = a2y - a1y;
|
const nx = b2x - b1x;
|
const ny = b2y - b1y;
|
|
const nmCrossProduct = crossProduct2d(nx, ny, mx, my);
|
if (Math.abs(nmCrossProduct) < 1e-6) {
|
return null;
|
}
|
|
const b1a1x = a1x - b1x;
|
const b1a1y = a1y - b1y;
|
|
const p = crossProduct2d(b1a1x, b1a1y, nx, ny) / nmCrossProduct;
|
if (p < 0 || p > 1) {
|
return null;
|
}
|
// p2 is an infinite line
|
return new Point(
|
p * mx + a1x,
|
p * my + a1y
|
);
|
}
|
|
function projPtOnLine(pt: Point, lineA: Point, lineB: Point): number {
|
const dir = new Point();
|
Point.sub(dir, lineB, lineA);
|
dir.normalize();
|
const dir2 = new Point();
|
Point.sub(dir2, pt, lineA);
|
const len = dir2.dot(dir);
|
return len;
|
}
|
|
function addToPoly(poly: number[][], pt: number[]) {
|
const last = poly[poly.length - 1];
|
if (last && last[0] === pt[0] && last[1] === pt[1]) {
|
return;
|
}
|
poly.push(pt);
|
}
|
|
function splitPolygonByLine(points: number[][], lineA: Point, lineB: Point) {
|
const len = points.length;
|
const intersections: {
|
projPt: number,
|
pt: Point
|
idx: number
|
}[] = [];
|
for (let i = 0; i < len; i++) {
|
const p0 = points[i];
|
const p1 = points[(i + 1) % len];
|
const intersectionPt = lineLineIntersect(
|
p0[0], p0[1], p1[0], p1[1],
|
lineA.x, lineA.y, lineB.x, lineB.y
|
);
|
if (intersectionPt) {
|
intersections.push({
|
projPt: projPtOnLine(intersectionPt, lineA, lineB),
|
pt: intersectionPt,
|
idx: i
|
});
|
}
|
}
|
|
// TODO No intersection?
|
if (intersections.length < 2) {
|
// Do clone
|
return [ { points}, {points} ];
|
}
|
|
// Find two farthest points.
|
intersections.sort((a, b) => {
|
return a.projPt - b.projPt;
|
});
|
let splitPt0 = intersections[0];
|
let splitPt1 = intersections[intersections.length - 1];
|
if (splitPt1.idx < splitPt0.idx) {
|
const tmp = splitPt0;
|
splitPt0 = splitPt1;
|
splitPt1 = tmp;
|
}
|
|
const splitPt0Arr = [splitPt0.pt.x, splitPt0.pt.y];
|
const splitPt1Arr = [splitPt1.pt.x, splitPt1.pt.y];
|
|
const newPolyA: number[][] = [splitPt0Arr];
|
const newPolyB: number[][] = [splitPt1Arr];
|
|
for (let i = splitPt0.idx + 1; i <= splitPt1.idx; i++) {
|
addToPoly(newPolyA, points[i].slice());
|
}
|
addToPoly(newPolyA, splitPt1Arr);
|
// Close the path
|
addToPoly(newPolyA, splitPt0Arr);
|
|
for (let i = splitPt1.idx + 1; i <= splitPt0.idx + len; i++) {
|
addToPoly(newPolyB, points[i % len].slice());
|
}
|
addToPoly(newPolyB, splitPt0Arr);
|
// Close the path
|
addToPoly(newPolyB, splitPt1Arr);
|
|
return [{
|
points: newPolyA
|
}, {
|
points: newPolyB
|
}];
|
}
|
|
function binaryDividePolygon(
|
polygonShape: Pick<Polygon['shape'], 'points'>
|
) {
|
const points = polygonShape.points;
|
const min: number[] = [];
|
const max: number[] = [];
|
fromPoints(points, min, max);
|
const boundingRect = new BoundingRect(
|
min[0], min[1], max[0] - min[0], max[1] - min[1]
|
);
|
|
const width = boundingRect.width;
|
const height = boundingRect.height;
|
const x = boundingRect.x;
|
const y = boundingRect.y;
|
|
const pt0 = new Point();
|
const pt1 = new Point();
|
if (width > height) {
|
pt0.x = pt1.x = x + width / 2;
|
pt0.y = y;
|
pt1.y = y + height;
|
}
|
else {
|
pt0.y = pt1.y = y + height / 2;
|
pt0.x = x;
|
pt1.x = x + width;
|
}
|
return splitPolygonByLine(points, pt0, pt1);
|
}
|
|
|
function binaryDivideRecursive<T extends Path['shape']>(
|
divider: BinaryDivide, shape: T, count: number, out: T[]
|
): T[] {
|
if (count === 1) {
|
out.push(shape);
|
}
|
else {
|
const mid = Math.floor(count / 2);
|
const sub = divider(shape);
|
binaryDivideRecursive(divider, sub[0], mid, out);
|
binaryDivideRecursive(divider, sub[1], count - mid, out);
|
}
|
|
return out;
|
}
|
|
export function clone(path: Path, count: number) {
|
const paths = [];
|
for (let i = 0; i < count; i++) {
|
paths.push(clonePath(path));
|
}
|
return paths;
|
}
|
|
function copyPathProps(source: Path, target: Path) {
|
target.setStyle(source.style);
|
target.z = source.z;
|
target.z2 = source.z2;
|
target.zlevel = source.zlevel;
|
}
|
|
function polygonConvert(points: number[]): number[][] {
|
const out = [];
|
for (let i = 0; i < points.length;) {
|
out.push([points[i++], points[i++]]);
|
}
|
return out;
|
}
|
|
export function split(
|
path: Path, count: number
|
) {
|
const outShapes: Path['shape'][] = [];
|
const shape = path.shape;
|
let OutShapeCtor: new() => Path;
|
// TODO Use clone when shape size is small
|
switch (path.type) {
|
case 'rect':
|
divideRect(shape as Rect['shape'], count, outShapes as Rect['shape'][]);
|
OutShapeCtor = Rect;
|
break;
|
case 'sector':
|
divideSector(shape as Sector['shape'], count, outShapes as Sector['shape'][]);
|
OutShapeCtor = Sector;
|
break;
|
case 'circle':
|
divideSector({
|
r0: 0, r: shape.r, startAngle: 0, endAngle: Math.PI * 2,
|
cx: shape.cx, cy: shape.cy
|
} as Sector['shape'], count, outShapes as Sector['shape'][]);
|
OutShapeCtor = Sector;
|
break;
|
default:
|
const m = path.getComputedTransform();
|
const scale = m ? Math.sqrt(Math.max(m[0] * m[0] + m[1] * m[1], m[2] * m[2] + m[3] * m[3])) : 1;
|
const polygons = map(
|
pathToPolygons(path.getUpdatedPathProxy(), scale),
|
poly => polygonConvert(poly)
|
);
|
const polygonCount = polygons.length;
|
if (polygonCount === 0) {
|
binaryDivideRecursive(binaryDividePolygon, {
|
points: polygons[0]
|
}, count, outShapes);
|
}
|
else if (polygonCount === count) { // In case we only split batched paths to non-batched paths. No need to split.
|
for (let i = 0; i < polygonCount; i++) {
|
outShapes.push({
|
points: polygons[i]
|
} as Polygon['shape']);
|
}
|
}
|
else {
|
// Most complex case. Assign multiple subpath to each polygon based on it's area.
|
let totalArea = 0;
|
const items = map(polygons, poly => {
|
const min: number[] = [];
|
const max: number[] = [];
|
fromPoints(poly, min, max);
|
// TODO: polygon area?
|
const area = (max[1] - min[1]) * (max[0] - min[0]);
|
totalArea += area;
|
return { poly, area };
|
});
|
items.sort((a, b) => b.area - a.area);
|
|
let left = count;
|
for (let i = 0; i < polygonCount; i++) {
|
const item = items[i];
|
if (left <= 0) {
|
break;
|
}
|
|
const selfCount = i === polygonCount - 1
|
? left // Use the last piece directly
|
: Math.ceil(item.area / totalArea * count);
|
|
if (selfCount < 0) {
|
continue;
|
}
|
|
binaryDivideRecursive(binaryDividePolygon, {
|
points: item.poly
|
}, selfCount, outShapes);
|
left -= selfCount;
|
};
|
}
|
OutShapeCtor = Polygon;
|
break;
|
}
|
|
if (!OutShapeCtor) {
|
// Unkown split algorithm. Use clone instead
|
return clone(path, count);
|
}
|
const out: Path[] = [];
|
|
for (let i = 0; i < outShapes.length; i++) {
|
const subPath = new OutShapeCtor();
|
subPath.setShape(outShapes[i]);
|
copyPathProps(path, subPath);
|
out.push(subPath);
|
}
|
|
return out;
|
}
|