| 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; | 
| } |