feat: improve performance by cachine edges

This commit is contained in:
max_richter 2024-03-21 16:31:17 +01:00
parent 2df3035855
commit 5f2b2f59be
10 changed files with 237 additions and 74 deletions

View File

@ -21,7 +21,9 @@
"@threlte/flex": "^1.0.1", "@threlte/flex": "^1.0.1",
"@types/three": "^0.159.0", "@types/three": "^0.159.0",
"jsondiffpatch": "^0.6.0", "jsondiffpatch": "^0.6.0",
"three": "^0.159.0" "meshline": "^3.2.0",
"three": "^0.159.0",
"three.meshline": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@histoire/plugin-svelte": "^0.17.9", "@histoire/plugin-svelte": "^0.17.9",

View File

@ -1,4 +1,4 @@
<script context="module"> <script context="module" lang="ts">
const color = new Color(0x202020); const color = new Color(0x202020);
color.convertLinearToSRGB(); color.convertLinearToSRGB();
@ -9,63 +9,72 @@
color, color,
toneMapped: false, toneMapped: false,
}); });
const lineCache = new Map<number, BufferGeometry>();
const curve = new CubicBezierCurve(
new Vector2(0, 0),
new Vector2(0, 0),
new Vector2(0, 0),
new Vector2(0, 0),
);
</script> </script>
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras"; import { MeshLineMaterial } from "@threlte/extras";
import { MeshBasicMaterial, type Mesh, LineBasicMaterial } from "three"; import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three";
import { Color } from "three/src/math/Color.js"; import { Color } from "three/src/math/Color.js";
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector3 } from "three/src/math/Vector3.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry";
export let from: { x: number; y: number }; export let from: { x: number; y: number };
export let to: { x: number; y: number }; export let to: { x: number; y: number };
let samples = 5; let samples = 5;
const curve = new CubicBezierCurve( let geometry: BufferGeometry;
new Vector2(from.x, from.y),
new Vector2(from.x + 2, from.y),
new Vector2(to.x - 2, to.y),
new Vector2(to.x, to.y),
);
let points: Vector3[] = []; let lastId: number | null = null;
let last_from_x = 0; const primeA = 31;
let last_from_y = 0; const primeB = 37;
let mesh: Mesh; export const update = function () {
const new_x = to.x - from.x;
export const update = function (force = false) { const new_y = to.y - from.y;
if (!force) { const curveId = new_x * primeA + new_y * primeB;
const new_x = from.x + to.x; if (lastId === curveId) {
const new_y = from.y + to.y;
if (last_from_x === new_x && last_from_y === new_y) {
return; return;
} }
last_from_x = new_x;
last_from_y = new_y; const mid = new Vector2(new_x / 2, new_y / 2);
if (lineCache.has(curveId)) {
geometry = lineCache.get(curveId)!;
return;
} }
const mid = new Vector2((from.x + to.x) / 2, (from.y + to.y) / 2);
const length = Math.floor( const length = Math.floor(
Math.sqrt(Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2)) / 4, Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
); );
samples = Math.min(Math.max(10, length), 100); samples = Math.min(Math.max(10, length), 60);
curve.v0.set(from.x, from.y); curve.v0.set(0, 0);
curve.v1.set(mid.x, from.y); curve.v1.set(mid.x, 0);
curve.v2.set(mid.x, to.y); curve.v2.set(mid.x, new_y);
curve.v3.set(to.x, to.y); curve.v3.set(new_x, new_y);
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y)); const points = curve
.getPoints(samples)
.map((p) => new Vector3(p.x, 0, p.y))
.flat();
geometry = createEdgeGeometry(points);
lineCache.set(curveId, geometry);
}; };
update();
$: if (from || to) { $: if (from || to) {
update(); update();
} }
@ -91,14 +100,13 @@
<T.CircleGeometry args={[0.3, 16]} /> <T.CircleGeometry args={[0.3, 16]} />
</T.Mesh> </T.Mesh>
<T.Mesh position.y={0.5} bind:ref={mesh}> {#if geometry}
{#key samples} <T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}>
<MeshLineGeometry {points} />
{/key}
<MeshLineMaterial <MeshLineMaterial
width={4} width={4}
attenuate={false} attenuate={false}
color={color2} color={color2}
toneMapped={false} toneMapped={false}
/> />
</T.Mesh> </T.Mesh>
{/if}

View File

@ -0,0 +1,100 @@
import { BufferGeometry, Vector3, BufferAttribute } from 'three'
import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils'
export function createEdgeGeometry(points: Vector3[]) {
let shape = 'none'
let shapeFunction = (p: number) => 1
// When the component first runs we create the buffer geometry and allocate the buffer attributes
let pointCount = points.length
let counters: number[] = []
let counterIndex = 0
let side: number[] = []
let widthArray: number[] = []
let doubleIndex = 0
let uvArray: number[] = []
let uvIndex = 0
let indices: number[] = []
let indicesIndex = 0
if (shape === 'taper') {
shapeFunction = (p: number) => 1 * Math.pow(4 * p * (1 - p), 1)
}
for (let j = 0; j < pointCount; j++) {
const c = j / points.length
counters[counterIndex + 0] = c
counters[counterIndex + 1] = c
counterIndex += 2
setXY(side, doubleIndex, 1, -1)
let width = shape === 'none' ? 1 : shapeFunction(j / (pointCount - 1))
setXY(widthArray, doubleIndex, width, width)
doubleIndex += 2
setXYZW(uvArray, uvIndex, j / (pointCount - 1), 0, j / (pointCount - 1), 1)
uvIndex += 4
if (j < pointCount - 1) {
const n = j * 2
setXYZ(indices, indicesIndex, n + 0, n + 1, n + 2)
setXYZ(indices, indicesIndex + 3, n + 2, n + 1, n + 3)
indicesIndex += 6
}
}
const geometry = new BufferGeometry()
// create these buffer attributes at the correct length but leave them empty for now
geometry.setAttribute('position', new BufferAttribute(new Float32Array(pointCount * 6), 3))
geometry.setAttribute('previous', new BufferAttribute(new Float32Array(pointCount * 6), 3))
geometry.setAttribute('next', new BufferAttribute(new Float32Array(pointCount * 6), 3))
// create and populate these buffer attributes
geometry.setAttribute('counters', new BufferAttribute(new Float32Array(counters), 1))
geometry.setAttribute('side', new BufferAttribute(new Float32Array(side), 1))
geometry.setAttribute('width', new BufferAttribute(new Float32Array(widthArray), 1))
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvArray), 2))
geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1))
let positions: number[] = []
let previous: number[] = []
let next: number[] = []
let positionIndex = 0
let previousIndex = 0
let nextIndex = 0
setXYZXYZ(previous, previousIndex, points[0].x, points[0].y, points[0].z)
previousIndex += 6
for (let j = 0; j < pointCount; j++) {
const p = points[j]
setXYZXYZ(positions, positionIndex, p.x, p.y, p.z)
positionIndex += 6
if (j < pointCount - 1) {
setXYZXYZ(previous, previousIndex, p.x, p.y, p.z)
previousIndex += 6
}
if (j > 0 && j + 1 <= pointCount) {
setXYZXYZ(next, nextIndex, p.x, p.y, p.z)
nextIndex += 6
}
}
setXYZXYZ(
next,
nextIndex,
points[pointCount - 1].x,
points[pointCount - 1].y,
points[pointCount - 1].z
)
const positionAttribute = (geometry.getAttribute('position') as BufferAttribute).set(positions)
const previousAttribute = (geometry.getAttribute('previous') as BufferAttribute).set(previous)
const nextAttribute = (geometry.getAttribute('next') as BufferAttribute).set(next)
positionAttribute.needsUpdate = true
previousAttribute.needsUpdate = true
nextAttribute.needsUpdate = true
geometry.computeBoundingSphere()
return geometry;
}

View File

@ -0,0 +1,34 @@
export const setXYZXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
array[location + 0] = x
array[location + 1] = y
array[location + 2] = z
array[location + 3] = x
array[location + 4] = y
array[location + 5] = z
}
export const setXY = (array: number[], location: number, x: number, y: number) => {
array[location + 0] = x
array[location + 1] = y
}
export const setXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
array[location + 0] = x
array[location + 1] = y
array[location + 2] = z
}
export const setXYZW = (
array: number[],
location: number,
x: number,
y: number,
z: number,
w: number
) => {
array[location + 0] = x
array[location + 1] = y
array[location + 2] = z
array[location + 3] = w
}

View File

@ -749,9 +749,7 @@
/> />
{/if} {/if}
{#key $graphId}
<GraphView {nodes} {edges} {cameraPosition} /> <GraphView {nodes} {edges} {cameraPosition} />
{/key}
{:else if $status === "loading"} {:else if $status === "loading"}
<span>Loading</span> <span>Loading</span>
{:else if $status === "error"} {:else if $status === "error"}

View File

@ -15,6 +15,7 @@
const socketId = `${node.id}-${id}`; const socketId = `${node.id}-${id}`;
const graph = getGraphManager(); const graph = getGraphManager();
const graphId = graph.id;
const inputSockets = graph.inputSockets; const inputSockets = graph.inputSockets;
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket"); const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
@ -67,11 +68,12 @@
class="wrapper" class="wrapper"
class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)} class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)}
> >
{#key id && graphId}
<div class="content" class:disabled={$inputSockets.has(socketId)}> <div class="content" class:disabled={$inputSockets.has(socketId)}>
<NodeInput {node} {input} {id} /> <NodeInput {node} {input} {id} />
</div> </div>
{#if node.tmp?.type?.inputs?.[id].internal !== true} {#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
<div <div
class="large target" class="large target"
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
@ -85,6 +87,7 @@
tabindex="0" tabindex="0"
/> />
{/if} {/if}
{/key}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -137,6 +137,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
} }
async load(graph: Graph) { async load(graph: Graph) {
const a = performance.now();
this.loaded = false; this.loaded = false;
this.graph = graph; this.graph = graph;
this.status.set("loading"); this.status.set("loading");
@ -161,6 +164,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
this.status.set("idle"); this.status.set("idle");
this.loaded = true; this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`);
} }
@ -296,7 +300,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
const nodeType = this.nodeRegistry.getNode(type); const nodeType = this.nodeRegistry.getNode(type);
if (!nodeType) { if (!nodeType) {
console.error(`Node type not found: ${type}`); logger.error(`Node type not found: ${type}`);
return; return;
} }
@ -317,8 +321,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
// check if this exact edge already exists // check if this exact edge already exists
const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket); const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket);
if (existingEdge) { if (existingEdge) {
console.log("Edge already exists"); logger.error("Edge already exists", existingEdge);
console.log(existingEdge)
return; return;
}; };
@ -327,7 +330,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
const toSocketType = to.tmp?.type?.inputs?.[toSocket]?.type; const toSocketType = to.tmp?.type?.inputs?.[toSocket]?.type;
if (fromSocketType !== toSocketType) { if (fromSocketType !== toSocketType) {
console.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`); logger.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`);
return; return;
} }
@ -396,11 +399,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
getParentsOfNode(node: Node) { getParentsOfNode(node: Node) {
const parents = []; const parents = [];
const stack = node.tmp?.parents?.slice(0); const stack = node.tmp?.parents?.slice(0);
while (stack?.length) { while (stack?.length) {
if (parents.length > 1000000) { if (parents.length > 1000000) {
console.log("Infinite loop detected") logger.warn("Infinite loop detected")
break; break;
} }
const parent = stack.pop(); const parent = stack.pop();

View File

@ -117,7 +117,6 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const inputNode = node.tmp.inputNodes?.[key]; const inputNode = node.tmp.inputNodes?.[key];
if (inputNode) { if (inputNode) {
if (results[inputNode.id] === undefined) { if (results[inputNode.id] === undefined) {
console.log(inputNode, node)
throw new Error("Input node has no result"); throw new Error("Input node has no result");
} }
inputs[key] = results[inputNode.id]; inputs[key] = results[inputNode.id];

View File

@ -37,7 +37,7 @@
<br /> <br />
<button <button
on:click={() => on:click={() =>
graphManager.load(graphManager.createTemplate("grid", 5, 5))} graphManager.load(graphManager.createTemplate("grid", 10, 10))}
>load grid</button >load grid</button
> >
<br /> <br />

View File

@ -38,9 +38,15 @@ importers:
jsondiffpatch: jsondiffpatch:
specifier: ^0.6.0 specifier: ^0.6.0
version: 0.6.0 version: 0.6.0
meshline:
specifier: ^3.2.0
version: 3.2.0(three@0.159.0)
three: three:
specifier: ^0.159.0 specifier: ^0.159.0
version: 0.159.0 version: 0.159.0
three.meshline:
specifier: ^1.4.0
version: 1.4.0
devDependencies: devDependencies:
'@histoire/plugin-svelte': '@histoire/plugin-svelte':
specifier: ^0.17.9 specifier: ^0.17.9
@ -2832,6 +2838,14 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
/meshline@3.2.0(three@0.159.0):
resolution: {integrity: sha512-ZaJkC967GTuef7UBdO0rGPX544oIWaNo7tYedVHSoR2lje6RR16fX8IsgMxgxoYYERtjqsRWIYBSPBxG4QR84Q==}
peerDependencies:
three: '>=0.137'
dependencies:
three: 0.159.0
dev: false
/meshoptimizer@0.18.1: /meshoptimizer@0.18.1:
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
dev: false dev: false
@ -4002,6 +4016,10 @@ packages:
tweakpane: 3.1.10 tweakpane: 3.1.10
dev: false dev: false
/three.meshline@1.4.0:
resolution: {integrity: sha512-A8IsiMrWP8zmHisGDAJ76ZD7t/dOF/oCe/FUKNE6Bu01ZYEx8N6IlU/1Plb2aOZtAuWM2A8s8qS3hvY0OFuvOw==}
dev: false
/three@0.159.0: /three@0.159.0:
resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==} resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==}
dev: false dev: false