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",
"@types/three": "^0.159.0",
"jsondiffpatch": "^0.6.0",
"three": "^0.159.0"
"meshline": "^3.2.0",
"three": "^0.159.0",
"three.meshline": "^1.4.0"
},
"devDependencies": {
"@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);
color.convertLinearToSRGB();
@ -9,63 +9,72 @@
color,
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 lang="ts">
import { T } from "@threlte/core";
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { MeshBasicMaterial, type Mesh, LineBasicMaterial } from "three";
import { MeshLineMaterial } from "@threlte/extras";
import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three";
import { Color } from "three/src/math/Color.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 { createEdgeGeometry } from "./createEdgeGeometry";
export let from: { x: number; y: number };
export let to: { x: number; y: number };
let samples = 5;
const curve = new CubicBezierCurve(
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 geometry: BufferGeometry;
let points: Vector3[] = [];
let lastId: number | null = null;
let last_from_x = 0;
let last_from_y = 0;
const primeA = 31;
const primeB = 37;
let mesh: Mesh;
export const update = function (force = false) {
if (!force) {
const new_x = from.x + to.x;
const new_y = from.y + to.y;
if (last_from_x === new_x && last_from_y === new_y) {
return;
}
last_from_x = new_x;
last_from_y = new_y;
export const update = function () {
const new_x = to.x - from.x;
const new_y = to.y - from.y;
const curveId = new_x * primeA + new_y * primeB;
if (lastId === curveId) {
return;
}
const mid = new Vector2((from.x + to.x) / 2, (from.y + to.y) / 2);
const mid = new Vector2(new_x / 2, new_y / 2);
if (lineCache.has(curveId)) {
geometry = lineCache.get(curveId)!;
return;
}
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.v1.set(mid.x, from.y);
curve.v2.set(mid.x, to.y);
curve.v3.set(to.x, to.y);
curve.v0.set(0, 0);
curve.v1.set(mid.x, 0);
curve.v2.set(mid.x, new_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) {
update();
}
@ -91,14 +100,13 @@
<T.CircleGeometry args={[0.3, 16]} />
</T.Mesh>
<T.Mesh position.y={0.5} bind:ref={mesh}>
{#key samples}
<MeshLineGeometry {points} />
{/key}
<MeshLineMaterial
width={4}
attenuate={false}
color={color2}
toneMapped={false}
/>
</T.Mesh>
{#if geometry}
<T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}>
<MeshLineMaterial
width={4}
attenuate={false}
color={color2}
toneMapped={false}
/>
</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}
{#key $graphId}
<GraphView {nodes} {edges} {cameraPosition} />
{/key}
<GraphView {nodes} {edges} {cameraPosition} />
{:else if $status === "loading"}
<span>Loading</span>
{:else if $status === "error"}

View File

@ -15,6 +15,7 @@
const socketId = `${node.id}-${id}`;
const graph = getGraphManager();
const graphId = graph.id;
const inputSockets = graph.inputSockets;
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
@ -67,24 +68,26 @@
class="wrapper"
class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)}
>
<div class="content" class:disabled={$inputSockets.has(socketId)}>
<NodeInput {node} {input} {id} />
</div>
{#key id && graphId}
<div class="content" class:disabled={$inputSockets.has(socketId)}>
<NodeInput {node} {input} {id} />
</div>
{#if node.tmp?.type?.inputs?.[id].internal !== true}
<div
class="large target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
/>
<div
class="small target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
/>
{/if}
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
<div
class="large target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
/>
<div
class="small target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
/>
{/if}
{/key}
<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) {
const a = performance.now();
this.loaded = false;
this.graph = graph;
this.status.set("loading");
@ -161,6 +164,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
this.status.set("idle");
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);
if (!nodeType) {
console.error(`Node type not found: ${type}`);
logger.error(`Node type not found: ${type}`);
return;
}
@ -317,8 +321,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
// check if this exact edge already exists
const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket);
if (existingEdge) {
console.log("Edge already exists");
console.log(existingEdge)
logger.error("Edge already exists", existingEdge);
return;
};
@ -327,7 +330,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
const toSocketType = to.tmp?.type?.inputs?.[toSocket]?.type;
if (fromSocketType !== toSocketType) {
console.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`);
logger.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`);
return;
}
@ -396,11 +399,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
getParentsOfNode(node: Node) {
const parents = [];
const stack = node.tmp?.parents?.slice(0);
while (stack?.length) {
if (parents.length > 1000000) {
console.log("Infinite loop detected")
logger.warn("Infinite loop detected")
break;
}
const parent = stack.pop();

View File

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

View File

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

View File

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