UNPKG

@amarillion/helixgraph

Version:

A collection of graph algorithms for game development

140 lines (139 loc) 5.25 kB
import { mmArrayPush } from "./multimap.js"; /** * @param {*} source a starting node, typically one of the possible source nodes. * @param {*} isSource a function to determine if a given node is a source * @param {*} isSink a function to determine if a given node is a sink * @param {*} getAdjacent function that for given node, returns array [ edge, node ] pairs * * @result a structure containing: * getWeight, getLeft, getRight, isSoure, isSink and getAdjacent functions, * as well as the data for those functions. */ export function simplify(source, isSource, isSink, getAdjacent) { function followEdge(source, edge, next, visited) { const forwardChain = { edgeChain: [edge], nodeChain: [source], }; const reverseChain = { edgeChain: [], nodeChain: [], }; let prev = source; let current = next; while (true) { if (visited.has(current)) { // discarded because this path has already been visited return [null, null]; } let reverseStep = null; const nonReverseSteps = []; for (const step of getAdjacent(current)) { if (step[1] === prev) { reverseStep = step; // NB we discard any double reverse Steps } else { nonReverseSteps.push(step); } } const degree = nonReverseSteps.length + 1; const isKeyNode = isSource(current) || isSink(current) || degree > 2; if (isKeyNode) { // chain ends // final reverse step reverseChain.edgeChain.unshift(reverseStep[0]); reverseChain.nodeChain.unshift(current); return [{ nodeChain: forwardChain.nodeChain, edgeChain: forwardChain.edgeChain, left: source, right: current, weight: forwardChain.edgeChain.length }, { nodeChain: reverseChain.nodeChain, edgeChain: reverseChain.edgeChain, left: current, right: source, weight: reverseChain.edgeChain.length }]; } else if (degree === 1) { // dead end return [null, null]; } else { // degree must be 2. nonReverseStep length must be 1. visited.add(current); const forwardStep = nonReverseSteps[0]; forwardChain.edgeChain.push(forwardStep[0]); forwardChain.nodeChain.push(current); reverseChain.edgeChain.unshift(reverseStep[0]); reverseChain.nodeChain.unshift(current); prev = current; current = forwardStep[1]; } } } const result = { getWeight: (e) => e.weight, getLeft: (e) => e.left, getRight: (e) => e.right, isSource: isSource, isSink: isSink, sources: [], sinks: [], nodes: [], edgesByNode: new Map(), getAdjacent: function* (node) { const edges = result.edgesByNode.get(node) || []; for (const edge of edges) { yield edge; } // TODO: test } }; const visited = new Set(); const keyNodes = new Set(); const open = []; open.push(source); keyNodes.add(source); while (open.length > 0) { const current = open.shift(); if (visited.has(current)) continue; if (isSource(current)) { result.sources.push(current); } if (isSink(current)) { result.sinks.push(current); } visited.add(current); // console.log ("Opening node", { current }); for (const [edge, dest] of getAdjacent(current)) { // console.log (" Checking adjacent node", { edge, dest }); const chains = followEdge(current, edge, dest, visited); const [forwardEdge, reverseEdge] = chains; if (forwardEdge && reverseEdge) { // console.log (" Adding new edge and node", { node: newEdge.right, edge: newEdge }); mmArrayPush(result.edgesByNode, forwardEdge.left, [forwardEdge, forwardEdge.right]); mmArrayPush(result.edgesByNode, reverseEdge.left, [reverseEdge, reverseEdge.right]); if (!visited.has(forwardEdge.right)) { keyNodes.add(forwardEdge.right); open.push(forwardEdge.right); } } } } result.nodes = [...keyNodes.keys()]; return result; } /** * * Given a path of edges as returned by trackbackEdges from a simplified graph * Concatenate the edgeChains of each simplified edge * Reverse the edgeChains when necessary. */ export function flattenPath(path) { return path.reduce((acc, e) => acc.concat(e.edgeChain), []); }