UNPKG

@threlte/core

Version:

A 3D framework for the web, built on top of Svelte and Three.js

279 lines (278 loc) 10.8 kB
import mitt, {} from 'mitt'; export class DAG { allVertices = {}; /** Nodes that are fully unlinked */ isolatedVertices = {}; connectedVertices = {}; sortedConnectedValues = []; needsSort = false; emitter = mitt(); emit = this.emitter.emit.bind(this.emitter); on = this.emitter.on.bind(this.emitter); off = this.emitter.off.bind(this.emitter); get sortedVertices() { return this.mapNodes((value) => value); } moveToIsolated(key) { const vertex = this.connectedVertices[key]; if (!vertex) return; this.isolatedVertices[key] = vertex; delete this.connectedVertices[key]; } moveToConnected(key) { const vertex = this.isolatedVertices[key]; if (!vertex) return; this.connectedVertices[key] = vertex; delete this.isolatedVertices[key]; } getKey = (v) => { if (typeof v === 'object') { return v.key; } return v; }; add(key, value, options) { if (this.allVertices[key] && this.allVertices[key].value !== undefined) { throw new Error(`A node with the key ${key.toString()} already exists`); } let vertex = this.allVertices[key]; if (!vertex) { vertex = { value: value, previous: new Set(), next: new Set() }; // add the vertex to the list of all vertices this.allVertices[key] = vertex; } else if (vertex.value === undefined) { vertex.value = value; } // if another node referenced this node before, we have inverse links const hasEdges = vertex.next.size > 0 || vertex.previous.size > 0; if (!options?.after && !options?.before && !hasEdges) { // the node we're about to add is fully unlinked this.isolatedVertices[key] = vertex; this.emit('node:added', { key, type: 'isolated', value }); return; } else { this.connectedVertices[key] = vertex; } if (options?.after) { const afterArr = Array.isArray(options.after) ? options.after : [options.after]; // we need to update the vertex to include the new "after" nodes afterArr.forEach((after) => { vertex.previous.add(this.getKey(after)); }); afterArr.forEach((after) => { const afterKey = this.getKey(after); // we get the vertex from the list of all vertices const linkedAfter = this.allVertices[afterKey]; if (!linkedAfter) { // if it doesn't exist, we create it this.allVertices[afterKey] = { value: undefined, // uninitialized previous: new Set(), next: new Set([key]) }; this.connectedVertices[afterKey] = this.allVertices[afterKey]; } else { // if it does exist, we update it linkedAfter.next.add(key); // we might need to move the vertex from isolated to connected this.moveToConnected(afterKey); } }); } if (options?.before) { const beforeArr = Array.isArray(options.before) ? options.before : [options.before]; // we need to update the vertex to include the new "before" nodes beforeArr.forEach((before) => { vertex.next.add(this.getKey(before)); }); beforeArr.forEach((before) => { const beforeKey = this.getKey(before); // we get the vertex from the list of all vertices const linkedBefore = this.allVertices[beforeKey]; if (!linkedBefore) { // if it doesn't exist, we create it this.allVertices[beforeKey] = { value: undefined, // uninitialized previous: new Set([key]), next: new Set() }; this.connectedVertices[beforeKey] = this.allVertices[beforeKey]; } else { // if it does exist, we update it linkedBefore.previous.add(key); // we might need to move the vertex from isolated to connected this.moveToConnected(beforeKey); } }); } this.emit('node:added', { key, type: 'connected', value }); // Mark the graph as needing a re-sort this.needsSort = true; } remove(key) { const removeKey = this.getKey(key); // check if it's an unlinked vertex const unlinkedVertex = this.isolatedVertices[removeKey]; if (unlinkedVertex) { delete this.isolatedVertices[removeKey]; delete this.allVertices[removeKey]; this.emit('node:removed', { key: removeKey, type: 'isolated' }); return; } // if it's not, it's a bit more complicated const linkedVertex = this.connectedVertices[removeKey]; if (!linkedVertex) { // The node does not exist in the graph. return; } // Update the 'next' nodes that this node points to linkedVertex.next.forEach((nextKey) => { const nextVertex = this.connectedVertices[nextKey]; if (nextVertex) { nextVertex.previous.delete(removeKey); if (nextVertex.previous.size === 0 && nextVertex.next.size === 0) { this.moveToIsolated(nextKey); } } }); // Update the 'previous' nodes that point to this node linkedVertex.previous.forEach((prevKey) => { const prevVertex = this.connectedVertices[prevKey]; if (prevVertex) { prevVertex.next.delete(removeKey); if (prevVertex.previous.size === 0 && prevVertex.next.size === 0) { this.moveToIsolated(prevKey); } } }); // Finally, remove the node from the graph delete this.connectedVertices[removeKey]; delete this.allVertices[removeKey]; this.emit('node:removed', { key: removeKey, type: 'connected' }); // Mark the graph as needing a re-sort this.needsSort = true; } mapNodes(callback) { if (this.needsSort) { this.sort(); } const result = []; this.forEachNode((value, index) => { result.push(callback(value, index)); }); return result; } forEachNode(callback) { if (this.needsSort) { this.sort(); } let index = 0; for (; index < this.sortedConnectedValues.length; index++) { callback(this.sortedConnectedValues[index], index); } Reflect.ownKeys(this.isolatedVertices).forEach((key) => { const vertex = this.isolatedVertices[key]; if (vertex.value !== undefined) callback(vertex.value, index++); }); } getValueByKey(key) { return this.allVertices[key]?.value; } getKeyByValue(value) { return (Reflect.ownKeys(this.connectedVertices).find((key) => this.connectedVertices[key].value === value) ?? Reflect.ownKeys(this.isolatedVertices).find((key) => this.isolatedVertices[key].value === value)); } sort() { const inDegree = new Map(); const zeroInDegreeQueue = []; const result = []; // we're only interested in vertices that have a value const connectedVertexKeysWithValues = Reflect.ownKeys(this.connectedVertices).filter((key) => { const vertex = this.connectedVertices[key]; return vertex.value !== undefined; }); // Initialize inDegree (count of incoming edges) for each vertex connectedVertexKeysWithValues.forEach((vertex) => { inDegree.set(vertex, 0); }); // Calculate inDegree for each vertex connectedVertexKeysWithValues.forEach((vertexKey) => { const vertex = this.connectedVertices[vertexKey]; vertex.next.forEach((next) => { // check if "next" vertex has a value const nextVertex = this.connectedVertices[next]; if (!nextVertex) return; inDegree.set(next, (inDegree.get(next) || 0) + 1); }); }); // Enqueue vertices with inDegree 0 inDegree.forEach((degree, value) => { if (degree === 0) { zeroInDegreeQueue.push(value); } }); // Process vertices with inDegree 0 and decrease inDegree of adjacent vertices while (zeroInDegreeQueue.length > 0) { const vertexKey = zeroInDegreeQueue.shift(); result.push(vertexKey); const v = connectedVertexKeysWithValues.find((key) => key === vertexKey); if (v) { this.connectedVertices[v]?.next.forEach((adjVertex) => { const adjVertexInDegree = (inDegree.get(adjVertex) || 0) - 1; inDegree.set(adjVertex, adjVertexInDegree); if (adjVertexInDegree === 0) { zeroInDegreeQueue.push(adjVertex); } }); } } // Check for cycles in the graph if (result.length !== connectedVertexKeysWithValues.length) { throw new Error('The graph contains a cycle, and thus can not be sorted topologically.'); } const filterUndefined = (value) => value !== undefined; this.sortedConnectedValues = result .map((key) => this.connectedVertices[key].value) .filter(filterUndefined); this.needsSort = false; } clear() { this.allVertices = {}; this.isolatedVertices = {}; this.connectedVertices = {}; this.sortedConnectedValues = []; this.needsSort = false; } static isKey(value) { return typeof value === 'string' || typeof value === 'symbol'; } static isValue(value) { return typeof value === 'object' && 'key' in value; } }