@threlte/core
Version:
A 3D framework for the web, built on top of Svelte and Three.js
279 lines (278 loc) • 10.8 kB
JavaScript
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;
}
}