From 7dceeda291c183abdbd4d05b286638462b53b14d Mon Sep 17 00:00:00 2001 From: Casper Lamboo Date: Thu, 24 May 2018 16:14:03 +0200 Subject: [PATCH] update combing --- comb.js | 104 +++----- src/sliceActions/helpers/comb.js | 385 +++++++++++++--------------- src/sliceActions/helpers/vector2.js | 1 + 3 files changed, 208 insertions(+), 282 deletions(-) diff --git a/comb.js b/comb.js index 78ec51b..180a67c 100644 --- a/comb.js +++ b/comb.js @@ -1,9 +1,9 @@ -import { pointIsInsideConvex, decompose, findClosestPath, containLineInPath } from './src/sliceActions/helpers/comb.js'; +import comb from './src/sliceActions/helpers/comb.js'; const canvas = document.createElement('canvas'); document.body.appendChild(canvas); -canvas.width = 610; -canvas.height = 610; +canvas.width = 800; +canvas.height = 800; const context = canvas.getContext('2d'); context.lineJoin = 'bevel'; @@ -21,27 +21,28 @@ function circle(radius = 10, x = 0, y = 0, clockWise = true, segments = 40) { return shape; } -const START = { x: 30, y: 550 }; +const START = { x: 200, y: 400 }; const END = { x: 400, y: 300 }; -// const CONCAVE_POLYGON = [[ -// { x: 10, y: 10 }, -// { x: 600, y: 10 }, -// { x: 500, y: 200 }, -// { x: 600, y: 600 }, -// { x: 10, y: 600 } -// ], [ -// { x: 160, y: 120 }, -// { x: 120, y: 400 }, -// { x: 400, y: 400 } -// ]]; -const CONCAVE_POLYGON = [ - circle(300, 305, 305, true), - circle(40, 305, 105, false), - circle(40, 305, 205, false), - circle(40, 305, 305, false), - circle(40, 305, 405, false), - circle(40, 305, 505, false) -]; + +const POLYGON = [[ + { x: 10, y: 10 }, + { x: 600, y: 10 }, + { x: 500, y: 200 }, + { x: 600, y: 600 }, + { x: 10, y: 600 } +], [ + { x: 160, y: 120 }, + { x: 120, y: 400 }, + { x: 400, y: 400 } +]]; +// const POLYGON = [ +// circle(300, 305, 305, true, 4), +// circle(40, 305, 105, false, 4), +// circle(40, 305, 205, false, 4), +// circle(40, 305, 305, false, 4), +// circle(40, 305, 405, false, 4), +// circle(40, 305, 505, false, 4) +// ]; canvas.onmousedown = (event) => { START.x = event.offsetX; @@ -56,20 +57,13 @@ canvas.onmousemove = (event) => { compute(); function compute() { - const { convexPolygons, vertices } = decompose(CONCAVE_POLYGON); - const startPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(START, face, vertices)); - const endPolygon = convexPolygons.findIndex(({ face }) => pointIsInsideConvex(END, face, vertices)); - if (startPolygon === -1 || endPolygon === -1) return; - - const path = findClosestPath(convexPolygons, startPolygon, endPolygon); - if (!path) return; - const line = containLineInPath(path, START, END, vertices); + const path = comb(POLYGON, START, END); // draw context.clearRect(0, 0, canvas.width, canvas.height); context.beginPath(); - for (const shape of CONCAVE_POLYGON) { + for (const shape of POLYGON) { let first = true; for (const { x, y } of shape) { if (first) { @@ -84,48 +78,12 @@ function compute() { context.fillStyle = 'lightgray'; context.fill(); - context.fillStyle = 'black'; - context.strokeStyle = 'black'; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.lineWidth = 1; - context.font = '14px arial'; - for (let i = 0; i < convexPolygons.length; i ++) { - const { face, center } = convexPolygons[i]; - - context.beginPath(); - for (const index of face) { - const vertex = vertices[index]; - context.lineTo(vertex.x, vertex.y); - } - context.closePath(); - context.stroke(); - - context.fillText(i, center.x, center.y); - } - - if (path) { - context.beginPath(); - for (const { edge: [indexA, indexB] } of path) { - const pointA = vertices[indexA]; - const pointB = vertices[indexB]; - context.moveTo(pointA.x, pointA.y); - context.lineTo(pointB.x, pointB.y); - } - context.strokeStyle = 'blue'; - context.lineWidth = 3; - context.stroke(); - } - - if (line) { - context.beginPath(); - for (const point of line) { - context.lineTo(point.x, point.y); - } - context.strokeStyle = 'green'; - context.lineWidth = 2; - context.stroke(); + context.beginPath(); + for (const { x, y } of path) { + context.lineTo(x, y); } + context.lineWidth = 2; + context.stroke(); context.beginPath(); context.arc(START.x, START.y, 3, 0, Math.PI * 2); diff --git a/src/sliceActions/helpers/comb.js b/src/sliceActions/helpers/comb.js index aa70756..a7fd1e1 100644 --- a/src/sliceActions/helpers/comb.js +++ b/src/sliceActions/helpers/comb.js @@ -1,228 +1,195 @@ -import { subtract, add, normalize, dot, distanceTo, divide, normal } from './vector2.js'; -import earcut from 'earcut'; +import { angle, subtract, distanceTo, normal } from './vector2.js'; -const TRIANGULATED_OUTLINES = new WeakMap(); -export default function comb(outline, start, end) { - if (distanceTo(start, end) < 3) return [start, end]; +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); - if (!TRIANGULATED_OUTLINES.has(outline)) TRIANGULATED_OUTLINES.set(outline, decompose(outline)); - const { convexPolygons, vertices } = TRIANGULATED_OUTLINES.get(outline); + points = [...points, start, end]; + graph = [...graph]; - 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]; + const startNode = createNode(graph, points, edges, normals, start); + const endNode = createNode(graph, points, edges, normals, end); - const path = findClosestPath(convexPolygons, startPolygon, endPolygon); - if (!path) return [start, end]; - - const line = containLineInPath(path, start, end, vertices); - return line; -} - -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; -} - -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]]; - - const n = normalize(normal(subtract(vertexB, vertexA))); - const p = subtract(point, vertexA); - - if (dot(p, n) < 0) return false; + 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]; + } } + + return result; +} + +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 graph = points.map(() => ([])); + for (let i = 0; i < points.length; i ++) { + const a = points[i]; + + for (let j = i + 1; j < points.length; j ++) { + const b = points[j]; + const nextPoint = nextPoints.get(a); + const previousPoint = previousPoints.get(a); + + if (lineIsVisible(previousPoint, nextPoint, edges, a, b)) { + const distance = distanceTo(a, b); + + const connectNodeA = graph[i]; + connectNodeA.push({ to: j, distance }); + + const connectNodeB = graph[j]; + connectNodeB.push({ to: i, distance }); + } + } + } + return { graph, edges, points, normals }; +} + +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 < graph.length; i ++) { + const b = points[i]; + + if (!lineIsVisible(previousPoint, nextPoint, edges, point, b)) continue; + + const distance = distanceTo(point, b); + node.push({ to: i, distance }); + + graph[i] = [...graph[i], { to, distance }]; + } + + return to; +} + +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 (lineCrossesEdges(edges, a, b)) return false; + return true; } -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 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; } + return false; +} - 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]; +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 overlap = []; - triangleIndexedA.face.map(index => { - if (triangleIndexedB.face.includes(index)) overlap.push(index); - }); + if (denominator === 0.0) return false; - 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 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); +} + +function normalizeAngle(a) { + a %= Math.PI * 2; + return a > 0.0 ? a : a + Math.PI * 2; +} + +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; +} + +// 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 [nodeIndex] = queue.splice(queueIndex, 1); + const node = graph[nodeIndex]; + + 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; } } } - return { vertices, convexPolygons }; + 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(); } -// 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; - } - } - } - - if (typeof distances[end] === 'undefined') return null; - - const nodes = []; - let node = end; - while (typeof node !== 'undefined') { - nodes.push(node); - node = predecessors[node]; - } - nodes.reverse(); - - const path = []; - for (let i = 1; i < nodes.length; i ++) { - const from = nodes[i - 1]; - const to = nodes[i]; - - const connection = map[from].connects.find(connect => connect.to === to); - path.push(connection); - } - - return path; -} - -export function containLineInPath(path, start, end, vertices) { - let line = [start]; - - 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]; - - 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; - - // line = containLineInPath(path.slice(0, i), start, newPoint, vertices); - line.push(newPoint); - } - } - - line.push(end); - return line; +function pointOnLine([a, b], point) { + return (a.x - point.x) * (a.y - point.y) === (b.x - point.x) * (b.y - point.y); } diff --git a/src/sliceActions/helpers/vector2.js b/src/sliceActions/helpers/vector2.js index bf10575..ad5d740 100644 --- a/src/sliceActions/helpers/vector2.js +++ b/src/sliceActions/helpers/vector2.js @@ -23,6 +23,7 @@ export const almostEquals = (a, b) => Math.abs(a.x - b.x) < 0.001 && Math.abs(a. export const dot = (a, b) => a.x * b.x + a.y * b.y; export const length = (v) => Math.sqrt(v.x * v.x + v.y * v.y); export const distanceTo = (a, b) => length(subtract(a, b)); +export const angle = (v) => Math.atan2(v.y, v.x); export const normalize = (v) => { const l = length(v);