UNPKG

tilemap-to-convexes

Version:

Merge solid polygons and decompose into convexes.

296 lines (264 loc) 10.8 kB
import {removeHoles, convexPartition} from 'poly-partition'; type Point = { x: number, y: number }; type Edge = [Point, Point]; type Edges = Edge[]; type Contour = Point[]; type Polygon = Contour[]; function area(a: Point, b: Point, c: Point) { return (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y); } function equals(a: Point, b: Point) { return a.x === b.x && a.y === b.y; } function vectorsAngle(v1x: number, v1y: number, v2x: number, v2y: number) { let angle = Math.atan2(v2y, v2x) - Math.atan2(v1y, v1x); if (angle < 0) { angle += Math.PI * 2; } return angle; } export default class MergePolygons { private polygons: Edges[] = []; addPolygon(...vertices: Point[]) { const edges: Edge[] = []; for (let i = 0, len = vertices.length; i < len; ++i) { const p0 = vertices[i]; const p1 = vertices[(i + 1) % len]; edges.push([{x: p0.x, y: p0.y}, {x: p1.x, y: p1.y}]); } this.polygons.push(edges); } getMergedPolygons(): Polygon[] { type WrappedEdge = { edge: Edge, polygon: number }; type EdgesMap = Map<string, Map<string, WrappedEdge>>; function hash(point: Point) { return point.x + ',' + point.y; } let edgesMap: EdgesMap = new Map(); const polyEdgesMap: Map<number, WrappedEdge[]> = new Map(); function addEdge(edge: WrappedEdge) { const p1 = hash(edge.edge[0]); let p1Edges: Map<string, WrappedEdge>; if (edgesMap.has(p1)) { p1Edges = edgesMap.get(p1)!; } else { p1Edges = new Map(); edgesMap.set(p1, p1Edges); } p1Edges.set(hash(edge.edge[1]), edge); let polyEdges = polyEdgesMap.get(edge.polygon); if (!polyEdges) { polyEdges = []; polyEdgesMap.set(edge.polygon, polyEdges); } polyEdges.push(edge); } function removeEdge(edge: WrappedEdge) { removeEdgeFromMap(edgesMap, edge); const polyEdges = polyEdgesMap.get(edge.polygon); if (polyEdges) { const index = polyEdges.indexOf(edge); if (index >= 0) { polyEdges.splice(index, 1); } } } function removeEdgeFromMap(map: EdgesMap, edge: WrappedEdge) { const p1Hash = hash(edge.edge[0]); const p1Edges: Map<string, WrappedEdge> | undefined = map.get(p1Hash); if (p1Edges) { const p2Hash = hash(edge.edge[1]); p1Edges.delete(p2Hash); } } function getEdge(p1: Point, p2: Point): WrappedEdge | undefined { const p1Hash = hash(p1); const p1Edges = edgesMap.get(p1Hash); if (p1Edges) { const p2Hash = hash(p2); return p1Edges.get(p2Hash); } } function takeOne(from: EdgesMap, to?: EdgesMap): WrappedEdge | undefined { for (let iter = from.entries(), next = iter.next(); !next.done; next = iter.next()) { if (!next.value) { continue; } const p1Edges: Map<string, WrappedEdge> = next.value[1]; for (let iter = p1Edges.entries(), next = iter.next(); !next.done; next = iter.next()) { if (!next.value) { continue; } p1Edges.delete(next.value[0]); const edge: WrappedEdge = next.value[1]; if (to) { const p1 = hash(edge.edge[0]); const p2 = hash(edge.edge[1]); let toP1Edges = to.get(p1); if (!toP1Edges) { toP1Edges = new Map(); to.set(p1, toP1Edges); } toP1Edges.set(p2, edge); } return edge; } } } function mergePolygons(target: number, merged: number) { const targetEdges = polyEdgesMap.get(target); const mergedEdges = polyEdgesMap.get(merged); if (mergedEdges) { mergedEdges.forEach(edge => edge.polygon = target); if (targetEdges) { targetEdges.push(...mergedEdges); } polyEdgesMap.delete(merged); } } // init for (let polyIndex = 0, polyNum = this.polygons.length; polyIndex < polyNum; ++polyIndex) { const poly = this.polygons[polyIndex]; for (let i = 0, len = poly.length; i < len; ++i) { const edge: WrappedEdge = { edge: poly[i], polygon: polyIndex }; addEdge(edge); } } // remove shared edges let visitedMap: EdgesMap = new Map(); for (let edge = takeOne(edgesMap, visitedMap); edge; edge = takeOne(edgesMap, visitedMap)) { const reverse = getEdge(edge.edge[1], edge.edge[0]); if (reverse) { removeEdge(edge); removeEdge(reverse); if (edge.polygon !== reverse.polygon) { mergePolygons(edge.polygon, reverse.polygon); } removeEdgeFromMap(visitedMap, edge); removeEdgeFromMap(visitedMap, reverse); } } // merge collinear edges edgesMap = visitedMap; visitedMap = new Map(); for (let edge = takeOne(edgesMap, visitedMap); edge; edge = takeOne(edgesMap, visitedMap)) { if (edge.polygon < 0) { continue; } const p1 = hash(edge.edge[1]); let nextEdge: WrappedEdge | undefined = undefined; let nextEdgesMap: Map<string, WrappedEdge> | undefined = undefined; let count = 0; [edgesMap, visitedMap].forEach(map => { nextEdgesMap = map.get(p1); if (nextEdgesMap) { const iter = nextEdgesMap.values(); for (let next = iter.next(); !next.done; next = iter.next()) { nextEdge = next.value; count += 1; } } }); if (!nextEdge || count > 1) { continue; } if (area(edge.edge[0], edge.edge[1], nextEdge!.edge[1]) === 0) { removeEdge(edge); removeEdge(nextEdge); if (edge.polygon !== nextEdge!.polygon) { throw new Error('Found invalid edge'); } addEdge({ edge: [edge.edge[0], nextEdge!.edge[1]], polygon: edge.polygon }); removeEdgeFromMap(visitedMap, edge); removeEdgeFromMap(visitedMap, nextEdge); } } // merge into rings edgesMap = visitedMap; const polygons: Map<number, Polygon> = new Map(); const holes: Map<number, Contour[]> = new Map(); for (let curr = takeOne(edgesMap); curr; curr = takeOne(edgesMap)) { const contour: Contour = []; const start = curr; const polyId = curr.polygon; let angleSum = 0; contour.push(curr.edge[0]); for (; ;) { const nextEdgesMap = edgesMap.get(hash(curr.edge[1])); if (!nextEdgesMap) { throw new Error('Failed to find closed ring'); } const candidates = Array.from(nextEdgesMap.values()).filter(edge => edge.polygon === polyId); if (!candidates.length) { throw new Error('Failed to find closed ring'); } let next = candidates[0]; let minAngle = Infinity; for (let i = 0, len = candidates.length; i < len; ++i) { const candidate = candidates[i]; const angle = vectorsAngle( next.edge[1].x - next.edge[0].x, next.edge[1].y - next.edge[0].y, curr.edge[0].x - curr.edge[1].x, curr.edge[0].y - curr.edge[1].y ); if (angle < minAngle) { next = candidate; minAngle = angle; } } contour.push(next.edge[0]); curr = next; angleSum += minAngle; removeEdgeFromMap(edgesMap, next); if (equals(next.edge[1], start.edge[0])) { break; } } const isHole = (angleSum - (contour.length - 2) * Math.PI) > Math.PI * 1e-4; if (isHole) { let polyHoles = holes.get(polyId); if (!polyHoles) { polyHoles = []; holes.set(polyId, polyHoles); } polyHoles.push(contour); } else { if (polygons.has(polyId)) { throw new Error('Polygon ID is duplicated'); } let poly: Contour[] = [contour]; polygons.set(polyId, poly); } } for (let iter = holes.entries(), next = iter.next(); !next.done; next = iter.next()) { const polyId = next.value[0]; const polyHoles = next.value[1]; const poly = polygons.get(polyId); if (!poly) { throw new Error('Failed to find outer contour for holes'); } poly.push(...polyHoles); } return Array.from(polygons.values()); } getConvexes() { const polygons = this.getMergedPolygons(); const ret: Contour[] = []; polygons.forEach(polygon => { const merged = removeHoles(polygon[0], polygon.slice(1)); const convexes = convexPartition(merged); ret.push(...convexes); }); return ret; } }