fix: integrate new grapg visualization

This commit is contained in:
Boris Arzentar 2025-10-27 01:13:54 +01:00
parent 74cf8bd7c7
commit 77a4b914e1
No known key found for this signature in database
GPG key ID: D5CC274C784807B7
29 changed files with 2361 additions and 43 deletions

View file

@ -13,9 +13,13 @@
"culori": "^4.0.1",
"d3-force-3d": "^3.0.6",
"next": "15.3.3",
"ngraph.forcelayout": "^3.3.1",
"ngraph.graph": "^20.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-force-graph-2d": "^1.27.1",
"three": "^0.175.0",
"troika-three-text": "^0.52.4",
"uuid": "^9.0.1"
},
"devDependencies": {
@ -25,6 +29,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/three": "^0.175.0",
"@types/uuid": "^9.0.8",
"eslint": "^9",
"eslint-config-next": "^15.3.3",
@ -1305,12 +1310,44 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true
},
"node_modules/@types/three": {
"version": "0.175.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.175.0.tgz",
"integrity": "sha512-ldMSBgtZOZ3g9kJ3kOZSEtZIEITmJOzu8eKVpkhf036GuNkM4mt0NXecrjCn5tMm1OblOF7dZehlaDypBfNokw==",
"dev": true,
"dependencies": {
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/three/node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
@ -1834,6 +1871,12 @@
"win32"
]
},
"node_modules/@webgpu/types": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz",
"integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==",
"dev": true
},
"node_modules/accessor-fn": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
@ -2124,6 +2167,14 @@
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
}
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -3381,6 +3432,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@ -4652,6 +4709,12 @@
"node": ">= 8"
}
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -4846,6 +4909,39 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/ngraph.events": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz",
"integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw=="
},
"node_modules/ngraph.forcelayout": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
"integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
"dependencies": {
"ngraph.events": "^1.0.0",
"ngraph.merge": "^1.0.0",
"ngraph.random": "^1.0.0"
}
},
"node_modules/ngraph.graph": {
"version": "20.1.0",
"resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.0.tgz",
"integrity": "sha512-1jorNgIc0Kg0L9bTNN4+RCrVvbZ+4pqGVMrbhX3LLyqYcRdLvAQRRnxddmfj9l5f6Eq59SUTfbYZEm8cktiE7Q==",
"dependencies": {
"ngraph.events": "^1.2.1"
}
},
"node_modules/ngraph.merge": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
"integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg=="
},
"node_modules/ngraph.random": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz",
"integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA=="
},
"node_modules/oauth4webapi": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.0.tgz",
@ -5275,6 +5371,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -5858,6 +5962,11 @@
"node": ">=18"
}
},
"node_modules/three": {
"version": "0.175.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.175.0.tgz",
"integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg=="
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
@ -5917,6 +6026,33 @@
"node": ">=8.0"
}
},
"node_modules/troika-three-text": {
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
"dependencies": {
"bidi-js": "^1.0.2",
"troika-three-utils": "^0.52.4",
"troika-worker-utils": "^0.52.0",
"webgl-sdf-generator": "1.1.1"
},
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/troika-three-utils": {
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/troika-worker-utils": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@ -6132,6 +6268,11 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/webgl-sdf-generator": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -17,7 +17,11 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-force-graph-2d": "^1.27.1",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"ngraph.forcelayout": "^3.3.1",
"ngraph.graph": "^20.1.0",
"three": "^0.175.0",
"troika-three-text": "^0.52.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@ -27,6 +31,7 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^9.0.8",
"@types/three": "^0.175.0",
"eslint": "^9",
"eslint-config-next": "^15.3.3",
"eslint-config-prettier": "^10.1.5",

View file

@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import { useBoolean } from "@/utils";
import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal, PopupMenu } from "@/ui/elements";
@ -258,15 +259,12 @@ export default function DatasetsAccordion({
tools={(
<IconButton className="relative">
<PopupMenu>
<div className="flex flex-col gap-0.5">
<div className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>add data</span>
</div>
</div>
<div className="flex flex-col gap-0.5 items-start">
<div onClick={() => handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</div>
<div className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>add data</span>
</div>
<Link target="_blank" href={`/visualize/${dataset.id}`}>visualize</Link>
<div onClick={() => handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</div>
</PopupMenu>
</IconButton>
)}

View file

@ -0,0 +1,38 @@
"use client";
import { useEffect, useState } from "react";
import { fetch } from "@/utils";
import { Edge, Node } from "@/ui/rendering/graph/types";
import GraphVisualization from "@/ui/elements/GraphVisualization";
interface VisualizePageProps {
params: { datasetId: string };
}
export default function Page({ params }: VisualizePageProps) {
const [graphData, setGraphData] = useState<{ nodes: Node[], edges: Edge[] }>();
useEffect(() => {
async function getData() {
const datasetId = (await params).datasetId;
const response = await fetch(`/v1/datasets/${datasetId}/graph`);
const newGraphData = await response.json();
setGraphData(newGraphData);
}
getData();
}, [params]);
return (
<div className="flex min-h-screen">
{graphData && (
<GraphVisualization
nodes={graphData.nodes}
edges={graphData.edges}
config={{
fontSize: 10,
}}
/>
)}
</div>
);
}

View file

@ -0,0 +1,35 @@
"use client";
import classNames from 'classnames';
import { useEffect, useRef } from "react";
import { Edge, Node } from "@/ui/rendering/graph/types";
import animate from "@/ui/rendering/animate";
interface GraphVisualizationProps {
nodes: Node[];
edges: Edge[];
className?: string;
config?: { fontSize: number };
}
export default function GraphVisualization({
nodes,
edges,
className,
config,
}: GraphVisualizationProps) {
const visualizationRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const visualizationContainer = visualizationRef.current;
if (visualizationContainer) {
animate(nodes, edges, visualizationContainer, config);
}
}, [config, edges, nodes]);
return (
<div className={classNames("min-w-full min-h-full", className)} ref={visualizationRef} />
);
}

View file

@ -2,16 +2,15 @@
import { v4 as uuid4 } from "uuid";
import classNames from "classnames";
import { Fragment, MouseEvent, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
import { Fragment, MouseEvent, useCallback, useEffect, useState } from "react";
import { useModal } from "@/ui/elements/Modal";
import { CaretIcon, CloseIcon, PlusIcon } from "@/ui/Icons";
import { IconButton, PopupMenu, TextArea, Modal, GhostButton, CTAButton } from "@/ui/elements";
import { GraphControlsAPI } from "@/app/(graph)/GraphControls";
import GraphVisualization, { GraphVisualizationAPI } from "@/app/(graph)/GraphVisualization";
import NotebookCellHeader from "./NotebookCellHeader";
import { Cell, Notebook as NotebookType } from "./types";
import GraphVisualization from "../GraphVisualization";
interface NotebookProps {
notebook: NotebookType;
@ -282,25 +281,23 @@ export default function Notebook({ notebook, updateNotebook, runCell }: Notebook
function CellResult({ content }: { content: [] }) {
const parsedContent = [];
const graphRef = useRef<GraphVisualizationAPI>();
const graphControls = useRef<GraphControlsAPI>({
setSelectedNode: () => {},
getSelectedNode: () => null,
});
for (const line of content) {
try {
if (Array.isArray(line)) {
// Insights search returns uncommon graph data structure
if (Array.from(line).length > 0 && Array.isArray(line[0]) && line[0][1]["relationship_name"]) {
const data = transformInsightsGraphData(line);
parsedContent.push(
<div key={line[0][1]["relationship_name"]} className="w-full h-full bg-white">
<div key={line[0][1]["relationship_name"]} className="flex flex-col w-full h-full min-h-80 bg-white">
<span className="text-sm pl-2 mb-4">reasoning graph</span>
<GraphVisualization
data={transformInsightsGraphData(line)}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
graphControls={graphControls}
className="min-h-80"
nodes={data.nodes}
edges={data.edges}
className="flex-1"
config={{
fontSize: 24,
}}
/>
</div>
);
@ -342,13 +339,15 @@ function CellResult({ content }: { content: [] }) {
if (typeof item === "object" && item["graphs"] && typeof item["graphs"] === "object") {
Object.entries<{ nodes: []; edges: []; }>(item["graphs"]).forEach(([datasetName, graph]) => {
parsedContent.push(
<div key={datasetName} className="w-full h-full bg-white">
<div key={datasetName} className="flex flex-col w-full h-full min-h-80 bg-white">
<span className="text-sm pl-2 mb-4">reasoning graph (datasets: {datasetName})</span>
<GraphVisualization
data={transformToVisualizationData(graph)}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
graphControls={graphControls}
className="min-h-80"
nodes={graph.nodes}
edges={graph.edges}
className="flex-1"
config={{
fontSize: 24,
}}
/>
</div>
);
@ -373,13 +372,15 @@ function CellResult({ content }: { content: [] }) {
if (typeof(line) === "object" && line["graphs"]) {
Object.entries<{ nodes: []; edges: []; }>(line["graphs"]).forEach(([datasetName, graph]) => {
parsedContent.push(
<div key={datasetName} className="w-full h-full bg-white">
<div key={datasetName} className="flex flex-col w-full h-full min-h-80 bg-white">
<span className="text-sm pl-2 mb-4">reasoning graph (datasets: {datasetName})</span>
<GraphVisualization
data={transformToVisualizationData(graph)}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
graphControls={graphControls}
className="min-h-80"
nodes={graph.nodes}
edges={graph.edges}
className="flex-1"
config={{
fontSize: 24,
}}
/>
</div>
);
@ -418,13 +419,6 @@ function CellResult({ content }: { content: [] }) {
};
function transformToVisualizationData(graph: { nodes: [], edges: [] }) {
return {
nodes: graph.nodes,
links: graph.edges,
};
}
type Triplet = [{
id: string,
name: string,
@ -445,8 +439,9 @@ function transformInsightsGraphData(triplets: Triplet[]) {
type: string,
}
} = {};
const links: {
const edges: {
[key: string]: {
id: string,
source: string,
target: string,
label: string,
@ -465,7 +460,8 @@ function transformInsightsGraphData(triplets: Triplet[]) {
type: triplet[2].type,
};
const linkKey = `${triplet[0]["id"]}_${triplet[1]["relationship_name"]}_${triplet[2]["id"]}`;
links[linkKey] = {
edges[linkKey] = {
id: linkKey,
source: triplet[0].id,
target: triplet[2].id,
label: triplet[1]["relationship_name"],
@ -474,6 +470,6 @@ function transformInsightsGraphData(triplets: Triplet[]) {
return {
nodes: Object.values(nodes),
links: Object.values(links),
edges: Object.values(edges),
};
}

View file

@ -0,0 +1,445 @@
import { Graph, Node as GraphNode, Link as GraphLink } from "ngraph.graph";
import {
Color,
DataTexture,
OrthographicCamera,
RGBAFormat,
Scene,
UnsignedByteType,
Vector2,
WebGLRenderer,
WebGLRenderTarget,
} from "three";
import { OrbitControls } from "three/examples/jsm/Addons.js";
import createForceLayout, { Layout } from "ngraph.forcelayout";
import { Edge, Node } from "./graph/types";
import createGraph from "./graph/createGraph";
import createLabel from "./meshes/createLabel";
import pickNodeIndex from "./picking/pickNodeIndex";
import createEdgeMesh from "./meshes/createEdgeMesh";
import createPickingMesh from "./meshes/createPickingMesh";
import createNodeSwarmMesh from "./meshes/createNodeSwarmMesh";
import createNodePositionsTexture from "./textures/createNodePositionsTexture";
import createDensityRenderTarget from "./render-targets/createDensityRenderTarget";
import createDensityAccumulatorMesh from "./meshes/createDensityAccumulatorMesh";
import createMetaballMesh from "./meshes/createMetaballMesh";
const INITIAL_CAMERA_DISTANCE = 2000;
interface Config {
fontSize: number;
}
export default function animate(
nodes: Node[],
edges: Edge[],
parentElement: HTMLElement,
config?: Config
): void {
const nodeLabelMap = new Map();
const edgeLabelMap = new Map();
const colorPalette = [
new Color("#5C10F4"),
new Color("#A550FF"),
new Color("#0DFF00"),
new Color("#F4F4F4"),
new Color("#D8D8D8"),
];
let lastColorIndex = 0;
const colorPerType = new Map();
function getColorForType(nodeType: string): Color {
if (colorPerType.has(nodeType)) {
return colorPerType.get(nodeType);
}
const color = colorPalette[lastColorIndex % colorPalette.length];
colorPerType.set(nodeType, color);
lastColorIndex += 1;
return color;
}
const mousePosition = new Vector2();
// Node related data
const nodeColors = new Float32Array(nodes.length * 3);
const nodeIndices = new Map();
const textureSize = Math.ceil(Math.sqrt(nodes.length));
const nodePositionsData = new Float32Array(textureSize * textureSize * 4);
let nodeIndex = 0;
function forNode(node: Node) {
const color = getColorForType(node.type);
nodeColors[nodeIndex * 3 + 0] = color.r;
nodeColors[nodeIndex * 3 + 1] = color.g;
nodeColors[nodeIndex * 3 + 2] = color.b;
nodePositionsData[nodeIndex * 4 + 0] = 0.0;
nodePositionsData[nodeIndex * 4 + 1] = 0.0;
nodePositionsData[nodeIndex * 4 + 2] = 0.0;
nodePositionsData[nodeIndex * 4 + 3] = 1.0;
nodeIndices.set(node.id, nodeIndex);
nodeIndex += 1;
}
// Node related data
const edgeIndices = new Float32Array(edges.length * 2);
let edgeIndex = 0;
function forEdge(edge: Edge) {
const fromIndex = nodeIndices.get(edge.source);
const toIndex = nodeIndices.get(edge.target);
edgeIndices[edgeIndex * 2 + 0] = fromIndex;
edgeIndices[edgeIndex * 2 + 1] = toIndex;
edgeIndex += 1;
}
// Graph creation and layout
const graph = createGraph(nodes, edges, forNode, forEdge);
const graphLayout = createForceLayout(graph, {
dragCoefficient: 1.0,
springLength: 200,
springCoefficient: 0.2,
gravity: -1000,
});
// Node Mesh
const nodePositionsTexture = createNodePositionsTexture(
nodes,
nodePositionsData
);
const nodeSwarmMesh = createNodeSwarmMesh(
nodes,
nodePositionsTexture,
nodeColors,
INITIAL_CAMERA_DISTANCE
);
const edgeMesh = createEdgeMesh(
edges,
nodePositionsTexture,
edgeIndices,
INITIAL_CAMERA_DISTANCE
);
// Density cloud setup
const densityCloudScene = new Scene();
const densityCloudTarget = createDensityRenderTarget(512);
const densityAccumulatorMesh = createDensityAccumulatorMesh(
nodes,
nodeColors,
nodePositionsTexture,
INITIAL_CAMERA_DISTANCE
);
const metaballMesh = createMetaballMesh(densityCloudTarget);
// const densityCloudDebugMesh = createDebugViewMesh(densityCloudTarget);
// Density cloud setup end
let pickedNodeIndex = -1;
const lastPickedNodeIndex = -1;
const pickNodeFromScene = (event: unknown) => {
pickedNodeIndex = pickNodeIndexFromScene(event as MouseEvent);
};
parentElement.addEventListener("mousemove", (event) => {
const rect = parentElement.getBoundingClientRect();
mousePosition.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mousePosition.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
pickNodeFromScene(event);
});
const scene = new Scene();
scene.background = new Color("#000000");
const renderer = new WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(parentElement.clientWidth, parentElement.clientHeight);
if (parentElement.children.length === 0) {
parentElement.appendChild(renderer.domElement);
}
// Setup camera
const aspect = parentElement.clientWidth / parentElement.clientHeight;
const frustumSize = INITIAL_CAMERA_DISTANCE;
const camera = new OrthographicCamera(
(-frustumSize * aspect) / 2,
(frustumSize * aspect) / 2,
frustumSize / 2,
-frustumSize / 2,
1,
5000
);
camera.position.set(0, 0, INITIAL_CAMERA_DISTANCE);
camera.lookAt(0, 0, 0);
// Setup controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = false;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minZoom = 1;
controls.maxZoom = 4;
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 0, 0);
controls.update();
// Handle resizing
window.addEventListener("resize", () => {
const aspect = parentElement.clientWidth / parentElement.clientHeight;
camera.left = (-frustumSize * aspect) / 2;
camera.right = (frustumSize * aspect) / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(parentElement.clientWidth, parentElement.clientHeight);
});
// Node picking setup
const pickingTarget = new WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
format: RGBAFormat,
type: UnsignedByteType,
depthBuffer: true,
stencilBuffer: false,
}
);
const pickingScene = new Scene();
function pickNodeIndexFromScene(event: MouseEvent): number {
pickingScene.add(pickingMesh);
const pickedNodeIndex = pickNodeIndex(
event,
renderer,
pickingScene,
camera,
pickingTarget
);
return pickedNodeIndex;
}
const pickingMesh = createPickingMesh(
nodes,
nodePositionsTexture,
nodeColors,
INITIAL_CAMERA_DISTANCE
);
renderer.domElement.addEventListener("mousedown", (event) => {
const pickedNodeIndex = pickNodeIndexFromScene(event);
console.log("Picked node index: ", pickedNodeIndex);
});
// Node picking setup end
// Setup scene
for (let i = 0; i < 500; i++) {
graphLayout.step();
}
let visibleLabels: unknown[] = [];
const entityTypeLabels: [string, unknown][] = [];
for (const node of nodes) {
if (node.type === "EntityType") {
const label = createLabel(node.label, config?.fontSize);
entityTypeLabels.push([node.id, label]);
}
}
// const processingStep = 0;
// Render loop
function render() {
graphLayout.step();
controls.update();
updateNodePositions(
nodes,
graphLayout,
nodePositionsData,
nodePositionsTexture
);
const textScale = Math.max(1, 4 / camera.zoom);
nodeSwarmMesh.material.uniforms.camDist.value = Math.floor(
camera.zoom * 500
);
nodeSwarmMesh.material.uniforms.mousePos.value.set(
mousePosition.x,
mousePosition.y
);
// @ts-expect-error uniforms does exist on material
edgeMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
// @ts-expect-error uniforms does exist on material
edgeMesh.material.uniforms.mousePos.value.set(
mousePosition.x,
mousePosition.y
);
// @ts-expect-error uniforms does exist on material
pickingMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
edgeMesh.renderOrder = 1;
nodeSwarmMesh.renderOrder = 2;
scene.add(edgeMesh);
scene.add(nodeSwarmMesh);
// Pass 1: draw points into density texture
renderer.setRenderTarget(densityCloudTarget);
renderer.clear();
densityCloudScene.clear();
densityCloudScene.add(densityAccumulatorMesh);
renderer.render(densityCloudScene, camera);
// Pass 2: render density map to screen
renderer.setRenderTarget(null);
renderer.clear();
metaballMesh.renderOrder = 0;
scene.add(metaballMesh);
for (const [nodeId, label] of entityTypeLabels) {
const nodePosition = graphLayout.getNodePosition(nodeId);
// @ts-expect-error label is Text from troika-three-text
label.position.set(nodePosition.x, nodePosition.y, 1.0);
// @ts-expect-error label is Text from troika-three-text
label.scale.setScalar(textScale);
// @ts-expect-error label is Text from troika-three-text
scene.add(label);
}
if (pickedNodeIndex >= 0) {
if (pickedNodeIndex !== lastPickedNodeIndex) {
for (const label of visibleLabels) {
// @ts-expect-error label is Text from troika-three-text
label.visible = false;
}
visibleLabels = [];
}
const pickedNode = nodes[pickedNodeIndex];
parentElement.style.cursor = "pointer";
const pickedNodePosition = graphLayout.getNodePosition(pickedNode.id);
let pickedNodeLabel = nodeLabelMap.get(pickedNode.id);
if (!pickedNodeLabel) {
pickedNodeLabel = createLabel(pickedNode.label, config?.fontSize);
nodeLabelMap.set(pickedNode.id, pickedNodeLabel);
}
pickedNodeLabel.position.set(
pickedNodePosition.x,
pickedNodePosition.y,
1.0
);
pickedNodeLabel.scale.setScalar(textScale);
if (camera.zoom > 2) {
graph.forEachLinkedNode(
pickedNode.id,
(otherNode: GraphNode, edge: GraphLink) => {
if (visibleLabels.length > 10) {
return;
}
let otherNodeLabel = nodeLabelMap.get(otherNode.id);
if (!otherNodeLabel) {
otherNodeLabel = createLabel(otherNode.data.label, config?.fontSize);
nodeLabelMap.set(otherNode.id, otherNodeLabel);
}
const otherNodePosition = graphLayout.getNodePosition(otherNode.id);
otherNodeLabel.position.set(
otherNodePosition.x,
otherNodePosition.y,
1.0
);
let linkLabel = edgeLabelMap.get(edge.id);
if (!linkLabel) {
linkLabel = createLabel(edge.data.label, config?.fontSize);
edgeLabelMap.set(edge.id, linkLabel);
}
const linkPosition = graphLayout.getLinkPosition(edge.id);
const middleLinkPosition = new Vector2(
(linkPosition.from.x + linkPosition.to.x) / 2,
(linkPosition.from.y + linkPosition.to.y) / 2
);
linkLabel.position.set(
middleLinkPosition.x,
middleLinkPosition.y,
1.0
);
linkLabel.visible = true;
linkLabel.scale.setScalar(textScale);
visibleLabels.push(linkLabel);
otherNodeLabel.visible = true;
otherNodeLabel.scale.setScalar(textScale);
visibleLabels.push(otherNodeLabel);
scene.add(linkLabel);
scene.add(otherNodeLabel);
}
);
}
pickedNodeLabel.visible = true;
visibleLabels.push(pickedNodeLabel);
scene.add(pickedNodeLabel);
} else {
parentElement.style.cursor = "default";
for (const label of visibleLabels) {
// @ts-expect-error label is Text from troika-three-text
label.visible = false;
}
visibleLabels = [];
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
}
function updateNodePositions(
nodes: Node[],
graphLayout: Layout<Graph>,
nodePositionsData: Float32Array,
nodePositionsTexture: DataTexture
) {
for (let i = 0; i < nodes.length; i++) {
const p = graphLayout.getNodePosition(nodes[i].id);
nodePositionsData[i * 4 + 0] = p.x;
nodePositionsData[i * 4 + 1] = p.y;
nodePositionsData[i * 4 + 2] = 0.0;
nodePositionsData[i * 4 + 3] = 1.0;
}
nodePositionsTexture.needsUpdate = true;
}

View file

@ -0,0 +1,28 @@
import createNgraph, { Graph } from "ngraph.graph";
import { Edge, Node } from "./types";
export default function createGraph(
nodes: Node[],
edges: Edge[],
forNode?: (node: Node) => void,
forEdge?: (node: Edge) => void
): Graph {
const graph = createNgraph();
for (const node of nodes) {
graph.addNode(node.id, {
id: node.id,
label: node.label,
});
forNode?.(node);
}
for (const edge of edges) {
graph.addLink(edge.source, edge.target, {
id: edge.id,
label: edge.label,
});
forEdge?.(edge);
}
return graph;
}

View file

@ -0,0 +1,12 @@
export interface Node {
id: string;
label: string;
type: string;
}
export interface Edge {
id: string;
label: string;
source: string;
target: string;
}

View file

@ -0,0 +1,46 @@
import { ShaderMaterial, Texture, Vector2 } from "three";
export function createBlurPassMaterial(
texture: Texture,
direction = new Vector2(1.0, 0.0)
) {
return new ShaderMaterial({
uniforms: {
densityTex: { value: texture },
direction: { value: direction }, // (1,0) = horizontal, (0,1) = vertical
texSize: { value: new Vector2(512, 512) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`,
fragmentShader: `
precision highp float;
uniform sampler2D densityTex;
uniform vec2 direction;
uniform vec2 texSize;
varying vec2 vUv;
void main() {
vec2 texel = direction / texSize;
float kernel[5];
kernel[0] = 0.204164;
kernel[1] = 0.304005;
kernel[2] = 0.193783;
kernel[3] = 0.072184;
kernel[4] = 0.025864;
vec4 sum = texture2D(densityTex, vUv) * kernel[0];
for (int i = 1; i < 5; i++) {
sum += texture2D(densityTex, vUv + texel * float(i)) * kernel[i];
sum += texture2D(densityTex, vUv - texel * float(i)) * kernel[i];
}
gl_FragColor = sum;
}
`,
});
}

View file

@ -0,0 +1,37 @@
import { ShaderMaterial, Texture } from "three";
export function createDebugViewMaterial(fieldTexture: Texture) {
return new ShaderMaterial({
uniforms: {
fieldTex: { value: fieldTexture },
},
vertexShader: `
// void main() {
// gl_Position = vec4(position, 1.0);
// }
varying vec2 vUv;
void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
`,
fragmentShader: `
uniform sampler2D fieldTex;
varying vec2 vUv;
void main() {
// gl_FragColor = texture2D(fieldTex, vUv);
float field = texture2D(fieldTex, vUv).r;
field = pow(field * 2.0, 0.5); // optional tone mapping
gl_FragColor = vec4(vec3(field), 1.0);
}
// precision highp float;
// uniform sampler2D fieldTex;
// void main() {
// vec2 uv = gl_FragCoord.xy / vec2(textureSize(fieldTex, 0));
// float field = texture2D(fieldTex, uv).r;
// // visualize the field as grayscale
// gl_FragColor = vec4(vec3(field), 1.0);
// }
`,
});
}

View file

@ -0,0 +1,81 @@
import { AdditiveBlending, DataTexture, ShaderMaterial } from "three";
export default function createDensityAccumulatorMaterial(
nodePositionsTexture: DataTexture,
initialCameraDistance: number
) {
const densityCloudMaterial = new ShaderMaterial({
depthWrite: false,
depthTest: false,
transparent: true,
blending: AdditiveBlending,
uniforms: {
nodePositionsTexture: {
value: nodePositionsTexture,
},
textureSize: {
value: nodePositionsTexture.image.width,
},
camDist: {
value: initialCameraDistance,
},
radius: { value: 0.05 },
},
vertexShader: `
uniform sampler2D nodePositionsTexture;
uniform float textureSize;
uniform float camDist;
attribute vec3 nodeColor;
varying vec3 vColor;
varying vec2 vUv;
varying float nodeSize;
vec3 getNodePos(float idx) {
float fx = mod(idx, textureSize);
float fy = floor(idx / textureSize);
vec2 uv = (vec2(fx, fy) + 0.5) / textureSize;
return texture2D(nodePositionsTexture, uv).xyz;
}
void main() {
vUv = uv;
vColor = nodeColor;
vec3 nodePos = getNodePos(float(gl_InstanceID));
float baseNodeSize = 8.0;
// Normalize camera distance into [0,1]
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
nodeSize = baseNodeSize * mix(10.0, 12.0, t);
vec3 transformed = nodePos + position * nodeSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec2 vUv;
varying float nodeSize;
varying vec3 vColor;
void main() {
vec2 pCoord = vUv - 0.5;
float distSq = dot(pCoord, pCoord) * 4.0;
if (distSq > 1.0) {
discard;
}
float radiusSq = (nodeSize / 2.0) * (nodeSize / 2.0);
float falloff = max(0.0, 1.0 - distSq);
float influence = radiusSq * falloff * falloff;
vec3 accumulatedColor = vColor * influence;
gl_FragColor = vec4(accumulatedColor, influence);
}
`,
});
return densityCloudMaterial;
}

View file

@ -0,0 +1,70 @@
import * as three from "three";
export default function createEdgeMaterial(
texture: three.DataTexture,
initialCameraDistance: number
): three.ShaderMaterial {
const material = new three.ShaderMaterial({
transparent: true,
depthWrite: false,
blending: three.AdditiveBlending,
uniforms: {
nodePosTex: { value: texture },
textureSize: { value: texture.image.width },
camDist: { value: initialCameraDistance },
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
color: { value: new three.Color(0xffffff) },
},
vertexShader: `
attribute vec2 edgeIndices;
uniform sampler2D nodePosTex;
uniform float textureSize;
uniform float camDist;
uniform vec2 mousePos;
varying float vFade;
varying float vHighlight;
vec3 getNodePos(float idx) {
float x = mod(idx, textureSize);
float y = floor(idx / textureSize);
vec2 uv = (vec2(x, y) + 0.5) / textureSize;
return texture2D(nodePosTex, uv).xyz;
}
void main() {
vec3 start = getNodePos(edgeIndices.x);
vec3 end = getNodePos(edgeIndices.y);
vec3 nodePos = mix(start, end, position.x);
// Project world-space position to clip-space
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
float distanceFromMouse = length(ndc.xy - mousePos);
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
vFade = smoothstep(500.0, 1500.0, camDist);
vFade = 0.2 * clamp(vFade, 0.0, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
}
`,
fragmentShader: `
precision highp float;
uniform vec3 color;
varying vec3 vColor;
varying float vFade;
varying float vHighlight;
void main() {
vec3 finalColor = mix(color, vec3(1.0), vHighlight * 0.8);
float alpha = mix(vFade, 0.8, vHighlight);
gl_FragColor = vec4(finalColor, alpha);
}
`,
});
return material;
}

View file

@ -0,0 +1,50 @@
import { ShaderMaterial, Texture } from "three";
export function createMetaballMaterial(fieldTexture: Texture) {
return new ShaderMaterial({
transparent: true,
uniforms: {
fieldTex: { value: fieldTexture },
threshold: { value: 25000.0 },
smoothing: { value: 5000.0 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec2 vUv;
uniform float threshold;
uniform float smoothing;
uniform sampler2D fieldTex;
void main() {
vec4 fieldData = texture2D(fieldTex, vUv);
vec3 accumulatedColor = fieldData.rgb;
float totalInfluence = fieldData.a;
vec3 finalColor = vec3(0.0);
if (totalInfluence > 0.0) {
finalColor = accumulatedColor / totalInfluence;
}
// Smooth transition around threshold
float alphaEdge = smoothstep(threshold - smoothing, threshold + smoothing, totalInfluence);
float alpha = alphaEdge * 0.3;
if (alpha < 0.01) {
discard;
}
gl_FragColor = vec4(finalColor, alpha);
}
`,
});
}

View file

@ -0,0 +1,70 @@
import * as three from "three";
export default function createNodeSwarmMaterial(
nodePositionsTexture: three.DataTexture,
initialCameraDistance: number
) {
const material = new three.ShaderMaterial({
transparent: true,
uniforms: {
nodePosTex: { value: nodePositionsTexture },
textureSize: { value: nodePositionsTexture.image.width },
camDist: { value: initialCameraDistance },
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
},
vertexShader: `
precision highp float;
uniform sampler2D nodePosTex;
uniform float textureSize;
uniform float camDist;
uniform vec2 mousePos;
attribute vec3 nodeColor;
varying vec3 vColor;
varying float vHighlight;
vec3 getNodePos(float idx) {
float size = textureSize;
float fx = mod(idx, size);
float fy = floor(idx / size);
vec2 uv = (vec2(fx, fy) + 0.5) / size;
return texture2D(nodePosTex, uv).xyz;
}
void main() {
vColor = nodeColor;
vec3 nodePos = getNodePos(float(gl_InstanceID));
// Project world-space position to clip-space
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
float distanceFromMouse = length(ndc.xy - mousePos);
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
float baseNodeSize = 8.0;
// Normalize camera distance into [0,1]
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
float nodeSize = baseNodeSize * mix(1.1, 1.3, t);
vec3 transformed = nodePos + position * nodeSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec3 vColor;
varying float vHighlight;
void main() {
vec3 finalColor = mix(vColor, vec3(1.0), vHighlight * 0.3);
gl_FragColor = vec4(finalColor, 1.0);
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`,
});
return material;
}

View file

@ -0,0 +1,65 @@
import * as three from "three";
export default function createPickingMaterial(
nodePositionsTexture: three.DataTexture,
initialCameraDistance: number
) {
const pickingMaterial = new three.ShaderMaterial({
depthTest: true,
depthWrite: true,
transparent: false,
blending: three.NoBlending,
uniforms: {
nodePosTex: { value: nodePositionsTexture },
textureSize: { value: nodePositionsTexture.image.width },
camDist: { value: initialCameraDistance },
},
vertexShader: `
precision highp float;
uniform sampler2D nodePosTex;
uniform float textureSize;
uniform float camDist;
varying vec3 vColor;
vec3 getNodePos(float idx) {
float size = textureSize;
float fx = mod(idx, size);
float fy = floor(idx / size);
vec2 uv = (vec2(fx, fy) + 0.5) / size;
return texture2D(nodePosTex, uv).xyz;
}
void main() {
float id = float(gl_InstanceID);
vec3 nodePos = getNodePos(id);
vColor = vec3(
mod(id, 256.0) / 255.0,
mod(floor(id / 256.0), 256.0) / 255.0,
floor(id / 65536.0) / 255.0
);
// vColor = vec3(fract(sin(id * 12.9898) * 43758.5453));
float baseNodeSize = 4.0;
// Normalize camera distance into [0,1]
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
float nodeSize = baseNodeSize * mix(1.0, 2.0, t);
vec3 transformed = nodePos + position * nodeSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`,
});
return pickingMaterial;
}

View file

@ -0,0 +1,13 @@
import { Mesh, PlaneGeometry, WebGLRenderTarget } from "three";
import { createDebugViewMaterial } from "../materials/createDebugViewMaterial";
export default function createDebugViewMesh(renderTarget: WebGLRenderTarget) {
const debugQuad = new Mesh(
new PlaneGeometry(2, 2),
createDebugViewMaterial(renderTarget.texture)
);
debugQuad.frustumCulled = false;
return debugQuad;
}

View file

@ -0,0 +1,33 @@
import {
InstancedBufferAttribute,
InstancedMesh,
DataTexture,
PlaneGeometry,
} from "three";
import { Node } from "../graph/types";
import createDensityAccumulatorMaterial from "../materials/createDensityAccumulatorMaterial";
export default function createDensityAccumulatorMesh(
nodes: Node[],
nodeColors: Float32Array,
nodePositionsTexture: DataTexture,
initialCameraDistance: number
) {
const geometry = new PlaneGeometry(2, 2);
const material = createDensityAccumulatorMaterial(
nodePositionsTexture,
initialCameraDistance
);
geometry.setAttribute(
"nodeColor",
new InstancedBufferAttribute(nodeColors, 3)
);
const mesh = new InstancedMesh(geometry, material, nodes.length);
mesh.frustumCulled = false;
return mesh;
}

View file

@ -0,0 +1,35 @@
import * as three from "three";
import createEdgeMaterial from "../materials/createEdgeMaterial";
import { Edge } from "../graph/types";
export default function createEdgeMesh(
edges: Edge[],
nodePositionTexture: three.DataTexture,
edgeIndices: Float32Array,
initialCameraDistance: number
): three.LineSegments {
const numberOfEdges = edges.length;
const instGeom = new three.InstancedBufferGeometry();
instGeom.setAttribute(
"position",
new three.BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0]), 3)
);
// instGeom.index = baseGeom.index;
instGeom.instanceCount = numberOfEdges;
instGeom.setAttribute(
"edgeIndices",
new three.InstancedBufferAttribute(edgeIndices, 2)
);
const material = createEdgeMaterial(
nodePositionTexture,
initialCameraDistance
);
const edgeMesh = new three.LineSegments(instGeom, material);
edgeMesh.frustumCulled = false;
return edgeMesh;
}

View file

@ -0,0 +1,24 @@
import { Color } from "three";
import { Text } from "troika-three-text";
const LABEL_FONT_SIZE = 14;
export default function createLabel(text = "", fontSize = LABEL_FONT_SIZE): Text {
const label = new Text();
label.text = text;
label.fontSize = fontSize;
label.color = new Color("#ffffff");
label.strokeColor = new Color("#ffffff");
label.outlineWidth = 2;
label.outlineColor = new Color("#000000");
label.outlineOpacity = 0.5;
label.anchorX = "center";
label.anchorY = "middle";
label.visible = true;
label.frustumCulled = false;
label.renderOrder = 5;
label.maxWidth = 200;
label.sync();
return label;
}

View file

@ -0,0 +1,15 @@
import { Mesh, PlaneGeometry, WebGLRenderTarget } from "three";
import { createMetaballMaterial } from "../materials/createMetaballMaterial";
export default function createMetaballMesh(
fieldRenderTarget: WebGLRenderTarget
) {
const quadGeo = new PlaneGeometry(2, 2);
const metaballMat = createMetaballMaterial(fieldRenderTarget.texture);
const quad = new Mesh(quadGeo, metaballMat);
quad.frustumCulled = false;
return quad;
}

View file

@ -0,0 +1,35 @@
import {
Mesh,
DataTexture,
CircleGeometry,
InstancedBufferAttribute,
InstancedBufferGeometry,
} from "three";
import { Node } from "../graph/types";
import createNodeSwarmMaterial from "../materials/createNodeSwarmMaterial";
export default function createNodeSwarmMesh(
nodes: Node[],
nodePositionsTexture: DataTexture,
nodeColors: Float32Array,
initialCameraDistance: number
) {
const nodeGeom = new CircleGeometry(2, 16);
const geom = new InstancedBufferGeometry();
geom.index = nodeGeom.index;
geom.instanceCount = nodes.length;
geom.setAttribute("position", nodeGeom.attributes.position);
geom.setAttribute("uv", nodeGeom.attributes.uv);
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
const material = createNodeSwarmMaterial(
nodePositionsTexture,
initialCameraDistance
);
const nodeSwarmMesh = new Mesh(geom, material);
nodeSwarmMesh.frustumCulled = false;
return nodeSwarmMesh;
}

View file

@ -0,0 +1,35 @@
import {
Mesh,
DataTexture,
CircleGeometry,
InstancedBufferGeometry,
InstancedBufferAttribute,
} from "three";
import { Node } from "../graph/types";
import createPickingMaterial from "../materials/createPickingMaterial";
export default function createPickingMesh(
nodes: Node[],
nodePositionsTexture: DataTexture,
nodeColors: Float32Array,
initialCameraDistance: number
): Mesh {
const nodeGeom = new CircleGeometry(2, 16);
const geom = new InstancedBufferGeometry();
geom.index = nodeGeom.index;
geom.instanceCount = nodes.length;
geom.setAttribute("position", nodeGeom.attributes.position);
geom.setAttribute("uv", nodeGeom.attributes.uv);
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
const pickingMaterial = createPickingMaterial(
nodePositionsTexture,
initialCameraDistance
);
const pickingMesh = new Mesh(geom, pickingMaterial);
pickingMesh.frustumCulled = false;
return pickingMesh;
}

View file

@ -0,0 +1,40 @@
import {
OrthographicCamera,
Scene,
WebGLRenderer,
WebGLRenderTarget,
} from "three";
const pixelBuffer = new Uint8Array(4);
export default function pickNodeIndex(
event: MouseEvent,
renderer: WebGLRenderer,
pickingScene: Scene,
camera: OrthographicCamera,
pickingRenderTarget: WebGLRenderTarget
) {
const rect = renderer.domElement.getBoundingClientRect();
// Convert from client coords to pixel coords in render target
const x =
((event.clientX - rect.left) / rect.width) * pickingRenderTarget.width;
const y =
pickingRenderTarget.height -
((event.clientY - rect.top) / rect.height) * pickingRenderTarget.height;
renderer.setRenderTarget(pickingRenderTarget);
renderer.clear();
renderer.render(pickingScene, camera);
renderer.readRenderTargetPixels(
pickingRenderTarget,
Math.floor(x),
Math.floor(y),
1,
1,
pixelBuffer
);
renderer.setRenderTarget(null);
const id = pixelBuffer[0] + pixelBuffer[1] * 256 + pixelBuffer[2] * 256 * 256;
return id || -1;
}

View file

@ -0,0 +1,12 @@
import { FloatType, LinearFilter, RGBAFormat, WebGLRenderTarget } from "three";
export default function createDensityRenderTarget(size = 512) {
return new WebGLRenderTarget(size, size, {
format: RGBAFormat,
type: FloatType,
minFilter: LinearFilter,
magFilter: LinearFilter,
depthBuffer: false,
stencilBuffer: false,
});
}

View file

@ -0,0 +1,28 @@
import * as three from "three";
import { Node } from "../graph/types";
export default function createNodePositionsTexture(
nodes: Node[],
nodePositionData: Float32Array
): three.DataTexture {
const textureSize = Math.ceil(Math.sqrt(nodes.length));
for (let i = 0; i < nodes.length; i++) {
nodePositionData[i * 4 + 0] = 0.0;
nodePositionData[i * 4 + 1] = 0.0;
nodePositionData[i * 4 + 2] = 0.0;
nodePositionData[i * 4 + 3] = 1.0;
}
const texture = new three.DataTexture(
nodePositionData,
textureSize,
textureSize,
three.RGBAFormat,
three.FloatType
);
texture.needsUpdate = true;
texture.minFilter = three.NearestFilter;
texture.magFilter = three.NearestFilter;
return texture;
}

View file

@ -0,0 +1,465 @@
// /* eslint-disable @typescript-eslint/no-explicit-any */
declare module "troika-three-text";
// import type { Color, Material, Object3D, Object3DEventMap } from "three";
// export class BatchedText {
// constructor(...args: any[]);
// add(...args: any[]): void;
// addText(...args: any[]): void;
// copy(...args: any[]): void;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// remove(...args: any[]): void;
// removeText(...args: any[]): void;
// sync(...args: any[]): void;
// updateBounds(...args: any[]): void;
// updateMatrixWorld(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export class GlyphsGeometry {
// constructor(...args: any[]);
// applyClipRect(...args: any[]): void;
// computeBoundingBox(...args: any[]): void;
// computeBoundingSphere(...args: any[]): void;
// updateAttributeData(...args: any[]): void;
// updateGlyphs(...args: any[]): void;
// }
// export class Text extends Object3D<Object3DEventMap> {
// public text: string;
// public fontSize: number;
// public color: Color;
// public anchorX;
// public anchorY;
// public font: string;
// public material: Material;
// constructor(...args: any[]): Object3D<Object3DEventMap>;
// clone(...args: any[]): Object3D<Object3DEventMap>;
// copy(...args: any[]): Object3D<Object3DEventMap>;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// localPositionToTextCoords(...args: any[]): void;
// onBeforeRender(...args: any[]): void;
// raycast(...args: any[]): void;
// sync(...args: any[]): void;
// worldPositionToTextCoords(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export function configureTextBuilder(config: any): void;
// export function createTextDerivedMaterial(baseMaterial: any): any;
// export function dumpSDFTextures(): void;
// export function fontResolverWorkerModule(...args: any[]): any;
// export function getCaretAtPoint(textRenderInfo: any, x: any, y: any): any;
// export function getSelectionRects(
// textRenderInfo: any,
// start: any,
// end: any
// ): any;
// export function getTextRenderInfo(args: any, callback: any): any;
// export function preloadFont(
// { font, characters, sdfGlyphSize }: any,
// callback: any
// ): void;
// export function typesetterWorkerModule(...args: any[]): any;
// export namespace BatchedText {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace Text {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace fontResolverWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }
// export namespace typesetterWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }

View file

@ -0,0 +1,465 @@
// /* eslint-disable @typescript-eslint/no-explicit-any */
declare module "troika-three-utils";
// import type { Color, Material, Object3D, Object3DEventMap } from "three";
// export class BatchedText {
// constructor(...args: any[]);
// add(...args: any[]): void;
// addText(...args: any[]): void;
// copy(...args: any[]): void;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// remove(...args: any[]): void;
// removeText(...args: any[]): void;
// sync(...args: any[]): void;
// updateBounds(...args: any[]): void;
// updateMatrixWorld(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export class GlyphsGeometry {
// constructor(...args: any[]);
// applyClipRect(...args: any[]): void;
// computeBoundingBox(...args: any[]): void;
// computeBoundingSphere(...args: any[]): void;
// updateAttributeData(...args: any[]): void;
// updateGlyphs(...args: any[]): void;
// }
// export class Text extends Object3D<Object3DEventMap> {
// public text: string;
// public fontSize: number;
// public color: Color;
// public anchorX;
// public anchorY;
// public font: string;
// public material: Material;
// constructor(...args: any[]): Object3D<Object3DEventMap>;
// clone(...args: any[]): Object3D<Object3DEventMap>;
// copy(...args: any[]): Object3D<Object3DEventMap>;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// localPositionToTextCoords(...args: any[]): void;
// onBeforeRender(...args: any[]): void;
// raycast(...args: any[]): void;
// sync(...args: any[]): void;
// worldPositionToTextCoords(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export function configureTextBuilder(config: any): void;
// export function createTextDerivedMaterial(baseMaterial: any): any;
// export function dumpSDFTextures(): void;
// export function fontResolverWorkerModule(...args: any[]): any;
// export function getCaretAtPoint(textRenderInfo: any, x: any, y: any): any;
// export function getSelectionRects(
// textRenderInfo: any,
// start: any,
// end: any
// ): any;
// export function getTextRenderInfo(args: any, callback: any): any;
// export function preloadFont(
// { font, characters, sdfGlyphSize }: any,
// callback: any
// ): void;
// export function typesetterWorkerModule(...args: any[]): any;
// export namespace BatchedText {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace Text {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace fontResolverWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }
// export namespace typesetterWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }

View file

@ -55,6 +55,7 @@ class DataDTO(OutDTO):
class GraphNodeDTO(OutDTO):
id: UUID
label: str
type: str
properties: dict