3 changed files with 193 additions and 267 deletions
@ -1,228 +1,195 @@
|
||||
import { subtract, add, normalize, dot, distanceTo, divide, normal } from './vector2.js'; |
||||
import earcut from 'earcut'; |
||||
|
||||
const TRIANGULATED_OUTLINES = new WeakMap(); |
||||
export default function comb(outline, start, end) { |
||||
if (distanceTo(start, end) < 3) return [start, end]; |
||||
|
||||
if (!TRIANGULATED_OUTLINES.has(outline)) TRIANGULATED_OUTLINES.set(outline, decompose(outline)); |
||||
const { convexPolygons, vertices } = TRIANGULATED_OUTLINES.get(outline); |
||||
import { angle, subtract, distanceTo, normal } from './vector2.js'; |
||||
|
||||
const graphs = new WeakMap(); |
||||
export default function comb(polygons, start, end) { |
||||
if (!graphs.has(polygons)) graphs.set(polygons, createGraph(polygons)); |
||||
let { edges, graph, points, normals } = graphs.get(polygons); |
||||
|
||||
points = [...points, start, end]; |
||||
graph = [...graph]; |
||||
|
||||
const startNode = createNode(graph, points, edges, normals, start); |
||||
const endNode = createNode(graph, points, edges, normals, end); |
||||
|
||||
let result; |
||||
if (graph[startNode].some(node => node.to === endNode)) { |
||||
result = [start, end]; |
||||
} else { |
||||
const path = shortestPath(graph, startNode, endNode); |
||||
if (path) { |
||||
result = path.map(index => points[index]); |
||||
} else { |
||||
result = [start, end]; |
||||
} |
||||
} |
||||
|
||||
const startPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(start, face, vertices)); |
||||
const endPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(end, face, vertices)); |
||||
if (startPolygon === -1 || endPolygon === -1) return [start, end]; |
||||
if (startPolygon === endPolygon) return [start, end]; |
||||
return result; |
||||
} |
||||
|
||||
const path = findClosestPath(convexPolygons, startPolygon, endPolygon); |
||||
if (!path) return [start, end]; |
||||
function createGraph(polygons) { |
||||
const points = []; |
||||
const edges = []; |
||||
const nextPoints = new WeakMap(); |
||||
const previousPoints = new WeakMap(); |
||||
const normals = new WeakMap(); |
||||
for (let i = 0; i < polygons.length; i ++) { |
||||
const polygon = polygons[i]; |
||||
for (let j = 0; j < polygon.length; j ++) { |
||||
const point = polygon[j]; |
||||
const nextPoint = polygon[(j + 1) % polygon.length]; |
||||
const previousPoint = polygon[(j - 1 + polygon.length) % polygon.length]; |
||||
|
||||
points.push(point); |
||||
edges.push([point, nextPoint]); |
||||
nextPoints.set(point, nextPoint); |
||||
previousPoints.set(point, previousPoint); |
||||
|
||||
normals.set(point, normal(subtract(nextPoint, point))); |
||||
} |
||||
} |
||||
|
||||
const line = containLineInPath(path, start, end, vertices); |
||||
return line; |
||||
} |
||||
const graph = points.map(() => ([])); |
||||
for (let i = 0; i < points.length; i ++) { |
||||
const a = points[i]; |
||||
|
||||
function lineIntersection(a1, a2, b1, b2) { |
||||
// source: http://mathworld.wolfram.com/Line-LineIntersection.html
|
||||
const intersection = { |
||||
x: ((a1.x * a2.y - a1.y * a2.x) * (b1.x - b2.x) - (a1.x - a2.x) * (b1.x * b2.y - b1.y * b2.x)) / ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x)), |
||||
y: ((a1.x * a2.y - a1.y * a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x * b2.y - b1.y * b2.x)) / ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x)) |
||||
}; |
||||
|
||||
const intersectionA = subtract(intersection, a1); |
||||
const directionA = subtract(a2, a1); |
||||
const normalA = normalize(directionA); |
||||
const distanceA = dot(normalA, intersectionA); |
||||
if (distanceA < 0 || distanceA > dot(normalA, directionA)) return false; |
||||
|
||||
const intersectionB = subtract(intersection, b1); |
||||
const directionB = subtract(b2, b1); |
||||
const normalB = normalize(directionB); |
||||
const distanceB = dot(normalB, intersectionB); |
||||
if (distanceB < 0 || distanceB > dot(normalB, directionB)) return false; |
||||
|
||||
return intersection; |
||||
} |
||||
for (let j = i + 1; j < points.length; j ++) { |
||||
const b = points[j]; |
||||
const nextPoint = nextPoints.get(a); |
||||
const previousPoint = previousPoints.get(a); |
||||
|
||||
export function pointIsInsideConvex(point, convex, vertices) { |
||||
for (let i = 0; i < convex.length; i ++) { |
||||
const vertexA = vertices[convex[i]]; |
||||
const vertexB = vertices[convex[(i + 1) % convex.length]]; |
||||
if (lineIsVisible(previousPoint, nextPoint, edges, a, b)) { |
||||
const distance = distanceTo(a, b); |
||||
|
||||
const n = normalize(normal(subtract(vertexB, vertexA))); |
||||
const p = subtract(point, vertexA); |
||||
const connectNodeA = graph[i]; |
||||
connectNodeA.push({ to: j, distance }); |
||||
|
||||
if (dot(p, n) < 0) return false; |
||||
const connectNodeB = graph[j]; |
||||
connectNodeB.push({ to: i, distance }); |
||||
} |
||||
} |
||||
} |
||||
return true; |
||||
return { graph, edges, points, normals }; |
||||
} |
||||
|
||||
export function decompose(polygon) { |
||||
const vertices = polygon.reduce((points, path) => { |
||||
points.push(...path); |
||||
return points; |
||||
}, []); |
||||
const flatVertices = vertices.reduce((points, { x, y }) => { |
||||
points.push(x, y); |
||||
return points; |
||||
}, []); |
||||
let offset = 0; |
||||
const holes = polygon |
||||
.map(path => offset += path.length) |
||||
.slice(0, -1); |
||||
|
||||
const flatTrainglesIndexed = earcut(flatVertices, holes); |
||||
const convexPolygons = []; |
||||
for (let i = 0; i < flatTrainglesIndexed.length; i += 3) { |
||||
const face = [ |
||||
flatTrainglesIndexed[i], |
||||
flatTrainglesIndexed[i + 1], |
||||
flatTrainglesIndexed[i + 2] |
||||
]; |
||||
const center = divide(face.reduce((total, point) => { |
||||
if (!total) { |
||||
return vertices[point]; |
||||
} else { |
||||
return add(total, vertices[point]); |
||||
} |
||||
}, null), face.length); |
||||
convexPolygons.push({ |
||||
center, |
||||
face, |
||||
connects: [] |
||||
}); |
||||
function createNode(graph, points, edges, normals, point) { |
||||
const node = []; |
||||
const to = graph.length; |
||||
graph.push(node); |
||||
|
||||
let previousPoint; |
||||
let nextPoint; |
||||
for (let j = 0; j < edges.length; j ++) { |
||||
const edge = edges[j]; |
||||
if (pointOnLine(edge, point)) [previousPoint, nextPoint] = edge; |
||||
} |
||||
|
||||
for (let i = 0; i < convexPolygons.length; i ++) { |
||||
for (let j = i + 1; j < convexPolygons.length; j ++) { |
||||
const triangleIndexedA = convexPolygons[i]; |
||||
const triangleIndexedB = convexPolygons[j]; |
||||
for (let i = 0; i < graph.length; i ++) { |
||||
const b = points[i]; |
||||
|
||||
const overlap = []; |
||||
triangleIndexedA.face.map(index => { |
||||
if (triangleIndexedB.face.includes(index)) overlap.push(index); |
||||
}); |
||||
if (!lineIsVisible(previousPoint, nextPoint, edges, point, b)) continue; |
||||
|
||||
if (overlap.length === 2) { |
||||
const distance = distanceTo(convexPolygons[i].center, convexPolygons[j].center); |
||||
triangleIndexedA.connects.push({ to: j, edge: overlap, distance }); |
||||
triangleIndexedB.connects.push({ to: i, edge: overlap, distance }); |
||||
} |
||||
} |
||||
const distance = distanceTo(point, b); |
||||
node.push({ to: i, distance }); |
||||
|
||||
graph[i] = [...graph[i], { to, distance }]; |
||||
} |
||||
|
||||
return { vertices, convexPolygons }; |
||||
return to; |
||||
} |
||||
|
||||
// const distanceMap = new WeakMap();
|
||||
// export function findClosestPath(convexPolygons, start, end, visited = [], path = [], distance = 0) {
|
||||
// if (start === end) return [];
|
||||
//
|
||||
// visited = [...visited, start];
|
||||
//
|
||||
// const { connects } = convexPolygons[start];
|
||||
//
|
||||
// const finish = connects.find(({ to }) => to === end);
|
||||
// if (finish) return [...path, finish];
|
||||
//
|
||||
// const posibilities = [];
|
||||
// for (let i = 0; i < connects.length; i ++) {
|
||||
// const connect = connects[i];
|
||||
// if (visited.includes(connect.to)) continue;
|
||||
//
|
||||
// const positibiltyDistance = distance + connect.distance;
|
||||
// const posibility = findClosestPath(convexPolygons, connect.to, end, visited, [...path, connect], positibiltyDistance);
|
||||
// if (posibility) {
|
||||
// posibilities.push(posibility);
|
||||
// distanceMap.set(posibility, positibiltyDistance);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (posibilities.length === 0) {
|
||||
// return null;
|
||||
// } else if (posibilities.length === 1) {
|
||||
// return posibilities[0];
|
||||
// } else if (posibilities.length > 1) {
|
||||
// return posibilities.sort((a, b) => distanceMap.get(a) - distanceMap.get(b))[0];
|
||||
// }
|
||||
// }
|
||||
|
||||
const findKey = _key => ({ key }) => _key === key; |
||||
export function findClosestPath(map, start, end) { |
||||
// dijkstra's algorithm
|
||||
const distances = { [start]: 0 }; |
||||
const open = [{ key: 0, nodes: [start] }]; |
||||
const predecessors = {}; |
||||
|
||||
while (open.length !== 0) { |
||||
const key = Math.min(...open.map(n => n.key).sort()); |
||||
const bucket = open.find(findKey(key)); |
||||
const node = bucket.nodes.shift(); |
||||
const currentDistance = key; |
||||
const { connects } = map[node]; |
||||
|
||||
if (bucket.nodes.length === 0) open.splice(open.indexOf(bucket), 1); |
||||
|
||||
for (let i = 0; i < connects.length; i ++) { |
||||
const { distance, to } = connects[i]; |
||||
const totalDistance = distance + currentDistance; |
||||
const vertexDistance = distances[to]; |
||||
|
||||
if ((typeof vertexDistance === 'undefined') || (vertexDistance > totalDistance)) { |
||||
distances[to] = totalDistance; |
||||
|
||||
let openNode = open.find(findKey(totalDistance)); |
||||
if (!openNode) { |
||||
openNode = { key: totalDistance, nodes: [] }; |
||||
open.push(openNode); |
||||
} |
||||
openNode.nodes.push(to); |
||||
|
||||
predecessors[to] = node; |
||||
} |
||||
} |
||||
function lineIsVisible(previousPoint, nextPoint, edges, a, b) { |
||||
if (b === nextPoint || b === previousPoint) return true; |
||||
|
||||
if (previousPoint && nextPoint) { |
||||
const angleLine = angle(subtract(b, a)); |
||||
const anglePrevious = angle(subtract(previousPoint, a)); |
||||
const angleNext = angle(subtract(nextPoint, a)); |
||||
if (betweenAngles(angleLine, anglePrevious, angleNext)) return false; |
||||
} |
||||
|
||||
if (typeof distances[end] === 'undefined') return null; |
||||
if (lineCrossesEdges(edges, a, b)) return false; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
const nodes = []; |
||||
let node = end; |
||||
while (typeof node !== 'undefined') { |
||||
nodes.push(node); |
||||
node = predecessors[node]; |
||||
function lineCrossesEdges(edges, a, b) { |
||||
for (let i = 0; i < edges.length; i ++) { |
||||
const [c, d] = edges[i]; |
||||
if (lineSegmentsCross(a, b, c, d)) return true; |
||||
} |
||||
nodes.reverse(); |
||||
return false; |
||||
} |
||||
|
||||
const path = []; |
||||
for (let i = 1; i < nodes.length; i ++) { |
||||
const from = nodes[i - 1]; |
||||
const to = nodes[i]; |
||||
function lineSegmentsCross(a, b, c, d) { |
||||
const denominator = ((b.x - a.x) * (d.y - c.y)) - ((b.y - a.y) * (d.x - c.x)); |
||||
|
||||
const connection = map[from].connects.find(connect => connect.to === to); |
||||
path.push(connection); |
||||
} |
||||
if (denominator === 0.0) return false; |
||||
|
||||
const numerator1 = ((a.y - c.y) * (d.x - c.x)) - ((a.x - c.x) * (d.y - c.y)); |
||||
const numerator2 = ((a.y - c.y) * (b.x - a.x)) - ((a.x - c.x) * (b.y - a.y)); |
||||
if (numerator1 === 0.0 || numerator2 === 0.0) return false; |
||||
|
||||
const r = numerator1 / denominator; |
||||
const s = numerator2 / denominator; |
||||
return (r > 0.0 && r < 1.0) && (s >= 0.0 && s <= 1.0); |
||||
} |
||||
|
||||
return path; |
||||
function normalizeAngle(a) { |
||||
a %= Math.PI * 2; |
||||
return a > 0.0 ? a : a + Math.PI * 2; |
||||
} |
||||
|
||||
export function containLineInPath(path, start, end, vertices) { |
||||
let line = [start]; |
||||
function betweenAngles(n, a, b) { |
||||
n = normalizeAngle(n); |
||||
a = normalizeAngle(a); |
||||
b = normalizeAngle(b); |
||||
return a < b ? a <= n && n <= b : a <= n || n <= b; |
||||
} |
||||
|
||||
for (let i = 0; i < path.length; i ++) { |
||||
const { edge: [indexA, indexB] } = path[i]; |
||||
const vertexA = vertices[indexA]; |
||||
const vertexB = vertices[indexB]; |
||||
const lastPoint = line[line.length - 1]; |
||||
// dijkstra's algorithm
|
||||
function shortestPath(graph, start, end) { |
||||
const distances = graph.map(() => Infinity); |
||||
distances[start] = 0; |
||||
const traverse = []; |
||||
const queue = graph.map((node, i) => i); |
||||
|
||||
while (queue.length > 0) { |
||||
let queueIndex; |
||||
let minDistance = Infinity; |
||||
for (let index = 0; index < queue.length; index ++) { |
||||
const nodeIndex = queue[index]; |
||||
const distance = distances[nodeIndex]; |
||||
if (distances[nodeIndex] < minDistance) { |
||||
queueIndex = index; |
||||
minDistance = distance; |
||||
} |
||||
} |
||||
|
||||
const intersection = lineIntersection(lastPoint, end, vertexA, vertexB); |
||||
if (!intersection) { |
||||
const distanceA = distanceTo(lastPoint, vertexA) + distanceTo(vertexA, end); |
||||
const distanceB = distanceTo(lastPoint, vertexB) + distanceTo(vertexB, end); |
||||
const newPoint = distanceA < distanceB ? vertexA : vertexB; |
||||
const [nodeIndex] = queue.splice(queueIndex, 1); |
||||
const node = graph[nodeIndex]; |
||||
|
||||
// line = containLineInPath(path.slice(0, i), start, newPoint, vertices);
|
||||
line.push(newPoint); |
||||
for (let i = 0; i < node.length; i ++) { |
||||
const child = node[i]; |
||||
const distance = distances[nodeIndex] + child.distance; |
||||
if (distance < distances[child.to]) { |
||||
distances[child.to] = distance; |
||||
traverse[child.to] = nodeIndex; |
||||
} |
||||
} |
||||
} |
||||
|
||||
line.push(end); |
||||
return line; |
||||
if (!traverse.hasOwnProperty(end)) return null; |
||||
|
||||
const path = [end]; |
||||
let nodeIndex = end; |
||||
do { |
||||
nodeIndex = traverse[nodeIndex]; |
||||
path.push(nodeIndex); |
||||
} while (nodeIndex !== start); |
||||
|
||||
return path.reverse(); |
||||
} |
||||
|
||||
function pointOnLine([a, b], point) { |
||||
return (a.x - point.x) * (a.y - point.y) === (b.x - point.x) * (b.y - point.y); |
||||
} |
||||
|
Loading…
Reference in new issue