feat: some shit
This commit is contained in:
		| @@ -14,7 +14,7 @@ | ||||
|         var value = JSON.parse(store); | ||||
|         var themes = ["dark", "light", "catppuccin"]; | ||||
|         if (themes[value.theme]) { | ||||
|           document.body.classList.add("theme-" + themes[value.theme]); | ||||
|           document.documentElement.classList.add("theme-" + themes[value.theme]); | ||||
|         } | ||||
|       } catch (e) { } | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import type { NodeDefinition, NodeRegistry } from "@nodes/types"; | ||||
|   import { onMount } from "svelte"; | ||||
|   import { onDestroy, onMount } from "svelte"; | ||||
|  | ||||
|   let mx = $state(0); | ||||
|   let my = $state(0); | ||||
| @@ -39,9 +39,10 @@ | ||||
|   } | ||||
|  | ||||
|   onMount(() => { | ||||
|     wrapper?.parentElement?.setAttribute("style", "cursor:help !important"); | ||||
|     const style = wrapper.parentElement?.style; | ||||
|     style?.setProperty("cursor", "help"); | ||||
|     return () => { | ||||
|       wrapper?.parentElement?.style.removeProperty("cursor"); | ||||
|       style?.removeProperty("cursor"); | ||||
|     }; | ||||
|   }); | ||||
| </script> | ||||
| @@ -91,8 +92,9 @@ | ||||
|     border-radius: 5px; | ||||
|     top: 10px; | ||||
|     left: 10px; | ||||
|     max-width: 250px; | ||||
|     border: 1px solid var(--outline); | ||||
|     z-index: 1000; | ||||
|     z-index: 10000; | ||||
|     display: none; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <script lang="ts"> | ||||
|   import { T } from "@threlte/core"; | ||||
|  | ||||
|   import BackgroundVert from "./Background.vert"; | ||||
|   import BackgroundFrag from "./Background.frag"; | ||||
|   import { colors } from "../graph/state.svelte"; | ||||
|   import { colors } from "../graph/colors.svelte"; | ||||
|   import { Color } from "three"; | ||||
|   import { appSettings } from "$lib/settings/app-settings.svelte"; | ||||
|  | ||||
|   type Props = { | ||||
|     minZoom: number; | ||||
| @@ -42,10 +42,10 @@ | ||||
|           value: [0, 1, 0], | ||||
|         }, | ||||
|         backgroundColor: { | ||||
|           value: new Color(0x171717), | ||||
|           value: colors["layer-0"].clone(), | ||||
|         }, | ||||
|         lineColor: { | ||||
|           value: new Color(0x111111), | ||||
|           value: colors["outline"].clone(), | ||||
|         }, | ||||
|         zoomLimits: { | ||||
|           value: [2, 50], | ||||
| @@ -55,8 +55,9 @@ | ||||
|         }, | ||||
|       }} | ||||
|       uniforms.camPos.value={cameraPosition} | ||||
|       uniforms.backgroundColor.value={$colors["layer-0"]} | ||||
|       uniforms.lineColor.value={$colors["outline"]} | ||||
|       uniforms.backgroundColor.value={appSettings.theme && | ||||
|         colors["layer-0"].clone()} | ||||
|       uniforms.lineColor.value={appSettings.theme && colors["outline"].clone()} | ||||
|       uniforms.zoomLimits.value={[minZoom, maxZoom]} | ||||
|       uniforms.dimensions.value={[width, height]} | ||||
|     /> | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| <script module lang="ts"> | ||||
|   import { colors } from "../graph/state.svelte"; | ||||
|   import { colors } from "../graph/colors.svelte"; | ||||
|  | ||||
|   const circleMaterial = new MeshBasicMaterial({ | ||||
|     color: get(colors).edge, | ||||
|     color: colors.edge.clone(), | ||||
|     toneMapped: false, | ||||
|   }); | ||||
|  | ||||
|   colors.subscribe((c) => { | ||||
|     circleMaterial.color.copy(c.edge.clone().convertSRGBToLinear()); | ||||
|   $effect.root(() => { | ||||
|     $effect(() => { | ||||
|       appSettings.theme; | ||||
|       circleMaterial.color = colors.edge.clone().convertSRGBToLinear(); | ||||
|     }) | ||||
|   }); | ||||
|  | ||||
|   const lineCache = new Map<number, BufferGeometry>(); | ||||
| @@ -23,23 +25,24 @@ | ||||
| <script lang="ts"> | ||||
|   import { T } from "@threlte/core"; | ||||
|   import { MeshLineMaterial } from "@threlte/extras"; | ||||
|   import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three"; | ||||
|   import { BufferGeometry,  MeshBasicMaterial, Vector3 } from "three"; | ||||
|   import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; | ||||
|   import { Vector2 } from "three/src/math/Vector2.js"; | ||||
|   import { createEdgeGeometry } from "./createEdgeGeometry.js"; | ||||
|   import { get } from "svelte/store"; | ||||
|     import { appSettings } from "$lib/settings/app-settings.svelte"; | ||||
|  | ||||
|   type Props = { | ||||
|     from: { x: number; y: number }; | ||||
|     to: { x: number; y: number }; | ||||
|     z:number; | ||||
|   }; | ||||
|  | ||||
|   const { from, to }: Props = $props(); | ||||
|  | ||||
|   let samples = 5; | ||||
|   const { from, to, z }: Props = $props(); | ||||
|  | ||||
|   let geometry: BufferGeometry|null = $state(null); | ||||
|  | ||||
|   const lineColor = $derived(appSettings.theme && colors.edge.clone().convertSRGBToLinear()); | ||||
|  | ||||
|   let lastId: number | null = null; | ||||
|  | ||||
|   const primeA = 31; | ||||
| @@ -63,7 +66,8 @@ | ||||
|     const length = Math.floor( | ||||
|       Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4, | ||||
|     ); | ||||
|     samples = Math.min(Math.max(10, length), 60) * 2; | ||||
|  | ||||
|     const samples = Math.max(length * 16, 10); | ||||
|  | ||||
|     curve.v0.set(0, 0); | ||||
|     curve.v1.set(mid.x, 0); | ||||
| @@ -79,13 +83,12 @@ | ||||
|     lineCache.set(curveId, geometry); | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   $effect(() => { | ||||
|     if (from || to) { | ||||
|       update(); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   const lineColor = $derived($colors.edge.clone().convertSRGBToLinear()); | ||||
| </script> | ||||
|  | ||||
| <T.Mesh | ||||
| @@ -110,6 +113,6 @@ | ||||
|  | ||||
| {#if geometry} | ||||
|   <T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}> | ||||
|     <MeshLineMaterial width={3} attenuate={false} color={lineColor} /> | ||||
|     <MeshLineMaterial width={Math.max(z*0.0001,0.00001)} color={lineColor}  /> | ||||
|   </T.Mesh> | ||||
| {/if} | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| <script lang="ts"> | ||||
|   import Edge from "./Edge.svelte"; | ||||
|  | ||||
|   type Props = { from: { x: number; y: number }; to: { x: number; y: number } }; | ||||
|   const { from, to }: Props = $props(); | ||||
|   type Props = { | ||||
|     from: { x: number; y: number }; | ||||
|     to: { x: number; y: number }; | ||||
|     z: number; | ||||
|   }; | ||||
|   const { from, to, z }: Props = $props(); | ||||
| </script> | ||||
|  | ||||
| <Edge {from} {to} /> | ||||
| <Edge {from} {to} {z} /> | ||||
|   | ||||
| @@ -1,11 +1,29 @@ | ||||
| import { BufferGeometry, Vector3, BufferAttribute } from 'three' | ||||
| import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js' | ||||
| import { BufferAttribute, BufferGeometry, Vector3 } from 'three'; | ||||
| import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js'; | ||||
|  | ||||
|  | ||||
| export function createEdgeGeometry(points: Vector3[]) { | ||||
|  | ||||
|   let shape = 'none' | ||||
|   let shapeFunction = (p: number) => 1 | ||||
|   const length = points[0].distanceTo(points[points.length - 1]); | ||||
|  | ||||
|   const startRadius = 10.5; | ||||
|   const constantWidth = 2; | ||||
|   const taperFraction = 0.8 / length; | ||||
|  | ||||
|   function ease(t: number) { | ||||
|     return t * t * (3 - 2 * t); | ||||
|   } | ||||
|   let shapeFunction = (alpha: number) => { | ||||
|     if (alpha < taperFraction) { | ||||
|       const easedAlpha = ease(alpha / taperFraction); | ||||
|       return startRadius + (constantWidth - startRadius) * easedAlpha; | ||||
|     } else if (alpha > 1 - taperFraction) { | ||||
|       const easedAlpha = ease((alpha - (1 - taperFraction)) / taperFraction); | ||||
|       return constantWidth + (startRadius - constantWidth) * easedAlpha; | ||||
|     } else { | ||||
|       return constantWidth; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // When the component first runs we create the buffer geometry and allocate the buffer attributes | ||||
|   let pointCount = points.length | ||||
| @@ -19,9 +37,7 @@ export function createEdgeGeometry(points: Vector3[]) { | ||||
|   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 | ||||
| @@ -30,7 +46,7 @@ export function createEdgeGeometry(points: Vector3[]) { | ||||
|     counterIndex += 2 | ||||
|  | ||||
|     setXY(side, doubleIndex, 1, -1) | ||||
|     let width = shape === 'none' ? 1 : shapeFunction(j / (pointCount - 1)) | ||||
|     let width = shapeFunction((j / (pointCount - 1))) | ||||
|     setXY(widthArray, doubleIndex, width, width) | ||||
|     doubleIndex += 2 | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import type { Edge, Graph, Node, NodeInput, NodeRegistry, Socket, } from "@nodes/types"; | ||||
| import { fastHashString } from "@nodes/utils"; | ||||
| import { writable, type Writable } from "svelte/store"; | ||||
| import { get, writable, type Writable } from "svelte/store"; | ||||
| import EventEmitter from "./helpers/EventEmitter.js"; | ||||
| import { createLogger } from "./helpers/index.js"; | ||||
| import throttle from "./helpers/throttle.js"; | ||||
| @@ -42,6 +42,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, " | ||||
|  | ||||
|   history: HistoryManager = new HistoryManager(); | ||||
|   execute = throttle(() => { | ||||
|     console.log("Props", get(this.nodes).values().find(n => n.type === "max/plantarium/gravity")?.props); | ||||
|     if (this.loaded === false) return; | ||||
|     this.emit("result", this.serialize()); | ||||
|   }, 10); | ||||
|   | ||||
| @@ -6,7 +6,6 @@ | ||||
|   } from "../helpers/index.js"; | ||||
|   import type { OrthographicCamera } from "three"; | ||||
|   import Background from "../background/Background.svelte"; | ||||
|   import type { GraphManager } from "../graph-manager.js"; | ||||
|   import { getContext, onMount, setContext } from "svelte"; | ||||
|   import Camera from "../Camera.svelte"; | ||||
|   import GraphView from "./GraphView.svelte"; | ||||
| @@ -23,14 +22,13 @@ | ||||
|   import { Canvas } from "@threlte/core"; | ||||
|   import { getGraphManager } from "./context.js"; | ||||
|  | ||||
|   const state = getGraphState(); | ||||
|   const graphState = getGraphState(); | ||||
|  | ||||
|   export let snapToGrid = true; | ||||
|   export let showGrid = true; | ||||
|   export let showHelp = false; | ||||
|  | ||||
|   let keymap = | ||||
|     getContext<ReturnType<typeof createKeyMap>>("keymap") || createKeyMap([]); | ||||
|   const keymap = getContext<ReturnType<typeof createKeyMap>>("keymap"); | ||||
|  | ||||
|   const manager = getGraphManager(); | ||||
|  | ||||
| @@ -179,7 +177,7 @@ | ||||
|   } | ||||
|  | ||||
|   setContext("setDownSocket", (socket: Socket) => { | ||||
|     state.activeSocket = socket; | ||||
|     graphState.activeSocket = socket; | ||||
|  | ||||
|     let { node, index, position } = socket; | ||||
|  | ||||
| @@ -198,14 +196,14 @@ | ||||
|     } | ||||
|  | ||||
|     mouseDown = position; | ||||
|     state.activeSocket = { | ||||
|     graphState.activeSocket = { | ||||
|       node, | ||||
|       index, | ||||
|       position, | ||||
|     }; | ||||
|  | ||||
|     state.possibleSockets = manager | ||||
|       .getPossibleSockets(state.activeSocket) | ||||
|     graphState.possibleSockets = manager | ||||
|       .getPossibleSockets(graphState.activeSocket) | ||||
|       .map(([node, index]) => { | ||||
|         return { | ||||
|           node, | ||||
| @@ -259,14 +257,15 @@ | ||||
|     let my = event.clientY - rect.y; | ||||
|  | ||||
|     mousePosition = projectScreenToWorld(mx, my); | ||||
|     hoveredNodeId = getNodeIdFromEvent(event); | ||||
|  | ||||
|     if (!mouseDown) return; | ||||
|  | ||||
|     // we are creating a new edge here | ||||
|     if (state.activeSocket || state.possibleSockets?.length) { | ||||
|     if (graphState.activeSocket || graphState.possibleSockets?.length) { | ||||
|       let smallestDist = 1000; | ||||
|       let _socket; | ||||
|       for (const socket of state.possibleSockets) { | ||||
|       for (const socket of graphState.possibleSockets) { | ||||
|         const dist = Math.sqrt( | ||||
|           (socket.position[0] - mousePosition[0]) ** 2 + | ||||
|             (socket.position[1] - mousePosition[1]) ** 2, | ||||
| @@ -279,9 +278,9 @@ | ||||
|  | ||||
|       if (_socket && smallestDist < 0.9) { | ||||
|         mousePosition = _socket.position; | ||||
|         state.hoveredSocket = _socket; | ||||
|         graphState.hoveredSocket = _socket; | ||||
|       } else { | ||||
|         state.hoveredSocket = null; | ||||
|         graphState.hoveredSocket = null; | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
| @@ -301,17 +300,17 @@ | ||||
|         const y = node.position[1]; | ||||
|         const height = getNodeHeight(node.type); | ||||
|         if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) { | ||||
|           state.selectedNodes?.add(node.id); | ||||
|           graphState.selectedNodes?.add(node.id); | ||||
|         } else { | ||||
|           state.selectedNodes?.delete(node.id); | ||||
|           graphState.selectedNodes?.delete(node.id); | ||||
|         } | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // here we are handling dragging of nodes | ||||
|     if (state.activeNodeId !== -1 && mouseDownId !== -1) { | ||||
|       const node = manager.getNode(state.activeNodeId); | ||||
|     if (graphState.activeNodeId !== -1 && mouseDownId !== -1) { | ||||
|       const node = manager.getNode(graphState.activeNodeId); | ||||
|       if (!node || event.buttons !== 1) return; | ||||
|  | ||||
|       node.tmp = node.tmp || {}; | ||||
| @@ -340,8 +339,8 @@ | ||||
|       const vecX = oldX - newX; | ||||
|       const vecY = oldY - newY; | ||||
|  | ||||
|       if (state.selectedNodes?.size) { | ||||
|         for (const nodeId of state.selectedNodes) { | ||||
|       if (graphState.selectedNodes?.size) { | ||||
|         for (const nodeId of graphState.selectedNodes) { | ||||
|           const n = manager.getNode(nodeId); | ||||
|           if (!n?.tmp) continue; | ||||
|           n.tmp.x = (n?.tmp?.downX || 0) - vecX; | ||||
| @@ -360,6 +359,7 @@ | ||||
|     } | ||||
|  | ||||
|     // here we are handling panning of camera | ||||
|     isPanning = true; | ||||
|     let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2]; | ||||
|     let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2]; | ||||
|  | ||||
| @@ -424,43 +424,46 @@ | ||||
|  | ||||
|     // if we clicked on a node | ||||
|     if (clickedNodeId !== -1) { | ||||
|       if (state.activeNodeId === -1) { | ||||
|         state.activeNodeId = clickedNodeId; | ||||
|       if (graphState.activeNodeId === -1) { | ||||
|         graphState.activeNodeId = clickedNodeId; | ||||
|         // if the selected node is the same as the clicked node | ||||
|       } else if (state.activeNodeId === clickedNodeId) { | ||||
|       } else if (graphState.activeNodeId === clickedNodeId) { | ||||
|         //$activeNodeId = -1; | ||||
|         // if the clicked node is different from the selected node and secondary | ||||
|       } else if (event.ctrlKey) { | ||||
|         state.selectedNodes = state.selectedNodes || new Set(); | ||||
|         state.selectedNodes.add(state.activeNodeId); | ||||
|         state.selectedNodes.delete(clickedNodeId); | ||||
|         state.activeNodeId = clickedNodeId; | ||||
|         graphState.selectedNodes.add(graphState.activeNodeId); | ||||
|         graphState.selectedNodes.delete(clickedNodeId); | ||||
|         graphState.activeNodeId = clickedNodeId; | ||||
|         // select the node | ||||
|       } else if (event.shiftKey) { | ||||
|         const activeNode = manager.getNode(state.activeNodeId); | ||||
|         const activeNode = manager.getNode(graphState.activeNodeId); | ||||
|         const newNode = manager.getNode(clickedNodeId); | ||||
|         if (activeNode && newNode) { | ||||
|           const edge = manager.getNodesBetween(activeNode, newNode); | ||||
|           if (edge) { | ||||
|             const selected = new Set(edge.map((n) => n.id)); | ||||
|             selected.add(clickedNodeId); | ||||
|             state.selectedNodes = selected; | ||||
|             graphState.selectedNodes.clear(); | ||||
|             for (const node of edge) { | ||||
|               graphState.selectedNodes.add(node.id); | ||||
|             } | ||||
|             graphState.selectedNodes.add(clickedNodeId); | ||||
|           } | ||||
|         } | ||||
|       } else if (!state.selectedNodes?.has(clickedNodeId)) { | ||||
|         state.activeNodeId = clickedNodeId; | ||||
|         state.clearSelection(); | ||||
|       } else if (!graphState.selectedNodes.has(clickedNodeId)) { | ||||
|         graphState.activeNodeId = clickedNodeId; | ||||
|         graphState.clearSelection(); | ||||
|       } | ||||
|     } else if (event.ctrlKey) { | ||||
|       boxSelection = true; | ||||
|     } | ||||
|     const node = manager.getNode(state.activeNodeId); | ||||
|  | ||||
|     const node = manager.getNode(graphState.activeNodeId); | ||||
|     if (!node) return; | ||||
|     node.tmp = node.tmp || {}; | ||||
|     node.tmp.downX = node.position[0]; | ||||
|     node.tmp.downY = node.position[1]; | ||||
|     if (state.selectedNodes) { | ||||
|       for (const nodeId of state.selectedNodes) { | ||||
|  | ||||
|     if (graphState.selectedNodes) { | ||||
|       for (const nodeId of graphState.selectedNodes) { | ||||
|         const n = manager.getNode(nodeId); | ||||
|         if (!n) continue; | ||||
|         n.tmp = n.tmp || {}; | ||||
| @@ -471,8 +474,12 @@ | ||||
|   } | ||||
|  | ||||
|   function copyNodes() { | ||||
|     if (state.activeNodeId === -1 && !state.selectedNodes?.size) return; | ||||
|     let _nodes = [state.activeNodeId, ...(state.selectedNodes?.values() || [])] | ||||
|     if (graphState.activeNodeId === -1 && !graphState.selectedNodes?.size) | ||||
|       return; | ||||
|     let _nodes = [ | ||||
|       graphState.activeNodeId, | ||||
|       ...(graphState.selectedNodes?.values() || []), | ||||
|     ] | ||||
|       .map((id) => manager.getNode(id)) | ||||
|       .filter(Boolean) as Node[]; | ||||
|  | ||||
| @@ -508,7 +515,10 @@ | ||||
|       .filter(Boolean) as Node[]; | ||||
|  | ||||
|     const newNodes = manager.createGraph(_nodes, clipboard.edges); | ||||
|     state.selectedNodes = new Set(newNodes.map((n) => n.id)); | ||||
|     graphState.selectedNodes.clear(); | ||||
|     for (const node of newNodes) { | ||||
|       graphState.selectedNodes.add(node.id); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT"; | ||||
| @@ -517,12 +527,14 @@ | ||||
|     key: "l", | ||||
|     description: "Select linked nodes", | ||||
|     callback: () => { | ||||
|       const activeNode = manager.getNode(state.activeNodeId); | ||||
|       const activeNode = manager.getNode(graphState.activeNodeId); | ||||
|       if (activeNode) { | ||||
|         const nodes = manager.getLinkedNodes(activeNode); | ||||
|         state.selectedNodes = new Set(nodes.map((n) => n.id)); | ||||
|         graphState.selectedNodes.clear(); | ||||
|         for (const node of nodes) { | ||||
|           graphState.selectedNodes.add(node.id); | ||||
|         } | ||||
|       } | ||||
|       console.log(activeNode); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
| @@ -552,8 +564,8 @@ | ||||
|     key: "Escape", | ||||
|     description: "Deselect nodes", | ||||
|     callback: () => { | ||||
|       state.activeNodeId = -1; | ||||
|       state.clearSelection(); | ||||
|       graphState.activeNodeId = -1; | ||||
|       graphState.clearSelection(); | ||||
|       (document.activeElement as HTMLElement)?.blur(); | ||||
|     }, | ||||
|   }); | ||||
| @@ -605,7 +617,9 @@ | ||||
|     description: "Select all nodes", | ||||
|     callback: () => { | ||||
|       if (!isBodyFocused()) return; | ||||
|       state.selectedNodes = new Set($nodes.keys()); | ||||
|       for (const node of $nodes.keys()) { | ||||
|         graphState.selectedNodes.add(node); | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
| @@ -654,38 +668,39 @@ | ||||
|     callback: (event) => { | ||||
|       if (!isBodyFocused()) return; | ||||
|       manager.startUndoGroup(); | ||||
|       if (state.activeNodeId !== -1) { | ||||
|         const node = manager.getNode(state.activeNodeId); | ||||
|       if (graphState.activeNodeId !== -1) { | ||||
|         const node = manager.getNode(graphState.activeNodeId); | ||||
|         if (node) { | ||||
|           manager.removeNode(node, { restoreEdges: event.ctrlKey }); | ||||
|           state.activeNodeId = -1; | ||||
|           graphState.activeNodeId = -1; | ||||
|         } | ||||
|       } | ||||
|       if (state.selectedNodes) { | ||||
|         for (const nodeId of state.selectedNodes) { | ||||
|       if (graphState.selectedNodes) { | ||||
|         for (const nodeId of graphState.selectedNodes) { | ||||
|           const node = manager.getNode(nodeId); | ||||
|           if (node) { | ||||
|             manager.removeNode(node, { restoreEdges: event.ctrlKey }); | ||||
|           } | ||||
|         } | ||||
|         state.clearSelection(); | ||||
|         graphState.clearSelection(); | ||||
|       } | ||||
|       manager.saveUndoGroup(); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   function handleMouseUp(event: MouseEvent) { | ||||
|     isPanning = false; | ||||
|     if (!mouseDown) return; | ||||
|  | ||||
|     const activeNode = manager.getNode(state.activeNodeId); | ||||
|     const activeNode = manager.getNode(graphState.activeNodeId); | ||||
|  | ||||
|     const clickedNodeId = getNodeIdFromEvent(event); | ||||
|  | ||||
|     if (clickedNodeId !== -1) { | ||||
|       if (activeNode) { | ||||
|         if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) { | ||||
|           state.clearSelection(); | ||||
|           state.activeNodeId = clickedNodeId; | ||||
|           graphState.activeNodeId = clickedNodeId; | ||||
|           graphState.clearSelection(); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -708,7 +723,7 @@ | ||||
|         activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1]; | ||||
|       } | ||||
|       const nodes = [ | ||||
|         ...[...(state.selectedNodes?.values() || [])].map((id) => | ||||
|         ...[...(graphState.selectedNodes?.values() || [])].map((id) => | ||||
|           manager.getNode(id), | ||||
|         ), | ||||
|       ] as NodeType[]; | ||||
| @@ -747,26 +762,26 @@ | ||||
|         $edges = $edges; | ||||
|       }); | ||||
|       manager.save(); | ||||
|     } else if (state.hoveredSocket && state.activeSocket) { | ||||
|     } else if (graphState.hoveredSocket && graphState.activeSocket) { | ||||
|       if ( | ||||
|         typeof state.hoveredSocket.index === "number" && | ||||
|         typeof state.activeSocket.index === "string" | ||||
|         typeof graphState.hoveredSocket.index === "number" && | ||||
|         typeof graphState.activeSocket.index === "string" | ||||
|       ) { | ||||
|         manager.createEdge( | ||||
|           state.hoveredSocket.node, | ||||
|           state.hoveredSocket.index || 0, | ||||
|           state.activeSocket.node, | ||||
|           state.activeSocket.index, | ||||
|           graphState.hoveredSocket.node, | ||||
|           graphState.hoveredSocket.index || 0, | ||||
|           graphState.activeSocket.node, | ||||
|           graphState.activeSocket.index, | ||||
|         ); | ||||
|       } else if ( | ||||
|         typeof state.activeSocket.index == "number" && | ||||
|         typeof state.hoveredSocket.index === "string" | ||||
|         typeof graphState.activeSocket.index == "number" && | ||||
|         typeof graphState.hoveredSocket.index === "string" | ||||
|       ) { | ||||
|         manager.createEdge( | ||||
|           state.activeSocket.node, | ||||
|           state.activeSocket.index || 0, | ||||
|           state.hoveredSocket.node, | ||||
|           state.hoveredSocket.index, | ||||
|           graphState.activeSocket.node, | ||||
|           graphState.activeSocket.index || 0, | ||||
|           graphState.hoveredSocket.node, | ||||
|           graphState.hoveredSocket.index, | ||||
|         ); | ||||
|       } | ||||
|       manager.save(); | ||||
| @@ -780,22 +795,25 @@ | ||||
|       cameraDown[1] === cameraPosition[1] && | ||||
|       isBodyFocused() | ||||
|     ) { | ||||
|       state.activeNodeId = -1; | ||||
|       state.clearSelection(); | ||||
|       graphState.activeNodeId = -1; | ||||
|       graphState.clearSelection(); | ||||
|     } | ||||
|  | ||||
|     mouseDown = null; | ||||
|     boxSelection = false; | ||||
|     state.activeSocket = null; | ||||
|     state.possibleSockets = []; | ||||
|     state.hoveredSocket = null; | ||||
|     graphState.activeSocket = null; | ||||
|     graphState.possibleSockets = []; | ||||
|     graphState.hoveredSocket = null; | ||||
|     addMenuPosition = null; | ||||
|   } | ||||
|  | ||||
|   let isPanning = false; | ||||
|   let isDragging = false; | ||||
|   let hoveredNodeId = -1; | ||||
|  | ||||
|   function handleMouseLeave() { | ||||
|     isDragging = false; | ||||
|     isPanning = false; | ||||
|   } | ||||
|  | ||||
|   function handleDrop(event: DragEvent) { | ||||
| @@ -865,16 +883,19 @@ | ||||
|   function handleDragEnter(e: DragEvent) { | ||||
|     e.preventDefault(); | ||||
|     isDragging = true; | ||||
|     isPanning = false; | ||||
|   } | ||||
|  | ||||
|   function handlerDragOver(e: DragEvent) { | ||||
|     isDragging = true; | ||||
|     e.preventDefault(); | ||||
|     isDragging = true; | ||||
|     isPanning = false; | ||||
|   } | ||||
|  | ||||
|   function handleDragEnd(e: DragEvent) { | ||||
|     isDragging = false; | ||||
|     e.preventDefault(); | ||||
|     isDragging = true; | ||||
|     isPanning = false; | ||||
|   } | ||||
|  | ||||
|   onMount(() => { | ||||
| @@ -893,6 +914,8 @@ | ||||
|   on:wheel={handleMouseScroll} | ||||
|   bind:this={wrapper} | ||||
|   class="graph-wrapper" | ||||
|   class:is-panning={isPanning} | ||||
|   class:is-hovering={hoveredNodeId !== -1} | ||||
|   aria-label="Graph" | ||||
|   role="button" | ||||
|   tabindex="0" | ||||
| @@ -916,9 +939,6 @@ | ||||
|   /> | ||||
|   <label for="drop-zone"></label> | ||||
|  | ||||
|   {#if showHelp} | ||||
|     <HelpView registry={manager.registry} /> | ||||
|   {/if} | ||||
|   <Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}> | ||||
|     <Camera bind:camera position={cameraPosition} /> | ||||
|  | ||||
| @@ -943,11 +963,12 @@ | ||||
|         <AddMenu bind:position={addMenuPosition} graph={manager} /> | ||||
|       {/if} | ||||
|  | ||||
|       {#if state.activeSocket} | ||||
|       {#if graphState.activeSocket} | ||||
|         <FloatingEdge | ||||
|           z={cameraPosition[2]} | ||||
|           from={{ | ||||
|             x: state.activeSocket.position[0], | ||||
|             y: state.activeSocket.position[1], | ||||
|             x: graphState.activeSocket.position[0], | ||||
|             y: graphState.activeSocket.position[1], | ||||
|           }} | ||||
|           to={{ x: mousePosition[0], y: mousePosition[1] }} | ||||
|         /> | ||||
| @@ -962,6 +983,10 @@ | ||||
|   </Canvas> | ||||
| </div> | ||||
|  | ||||
| {#if showHelp} | ||||
|   <HelpView registry={manager.registry} /> | ||||
| {/if} | ||||
|  | ||||
| <style> | ||||
|   .graph-wrapper { | ||||
|     position: relative; | ||||
| @@ -969,6 +994,15 @@ | ||||
|     transition: opacity 0.3s ease; | ||||
|     height: 100%; | ||||
|   } | ||||
|  | ||||
|   .is-hovering { | ||||
|     cursor: pointer; | ||||
|   } | ||||
|  | ||||
|   .is-panning { | ||||
|     cursor: grab; | ||||
|   } | ||||
|  | ||||
|   input { | ||||
|     position: absolute; | ||||
|     z-index: 1; | ||||
|   | ||||
| @@ -6,10 +6,23 @@ | ||||
|   import { getContext, onMount } from "svelte"; | ||||
|   import type { Writable } from "svelte/store"; | ||||
|   import { getGraphState } from "./state.svelte"; | ||||
|   import { useThrelte } from "@threlte/core"; | ||||
|   import { appSettings } from "$lib/settings/app-settings.svelte"; | ||||
|  | ||||
|   export let nodes: Writable<Map<number, NodeType>>; | ||||
|   export let edges: Writable<EdgeType[]>; | ||||
|   export let cameraPosition = [0, 0, 4]; | ||||
|   type Props = { | ||||
|     nodes: Writable<Map<number, NodeType>>; | ||||
|     edges: Writable<EdgeType[]>; | ||||
|     cameraPosition: [number, number, number]; | ||||
|   }; | ||||
|  | ||||
|   const { nodes, edges, cameraPosition = [0, 0, 4] }: Props = $props(); | ||||
|  | ||||
|   const { invalidate } = useThrelte(); | ||||
|  | ||||
|   $effect(() => { | ||||
|     appSettings.theme; | ||||
|     invalidate(); | ||||
|   }); | ||||
|  | ||||
|   const graphState = getGraphState(); | ||||
|  | ||||
| @@ -23,7 +36,6 @@ | ||||
|   function getEdgePosition(edge: EdgeType) { | ||||
|     const pos1 = getSocketPosition(edge[0], edge[1]); | ||||
|     const pos2 = getSocketPosition(edge[2], edge[3]); | ||||
|  | ||||
|     return [pos1[0], pos1[1], pos2[0], pos2[1]]; | ||||
|   } | ||||
|  | ||||
| @@ -41,6 +53,7 @@ | ||||
|   {@const pos = getEdgePosition(edge)} | ||||
|   {@const [x1, y1, x2, y2] = pos} | ||||
|   <Edge | ||||
|     z={cameraPosition[2]} | ||||
|     from={{ | ||||
|       x: x1, | ||||
|       y: y1, | ||||
|   | ||||
| @@ -3,19 +3,18 @@ | ||||
|   import GraphEl from "./Graph.svelte"; | ||||
|   import { GraphManager } from "../graph-manager.js"; | ||||
|   import { setContext } from "svelte"; | ||||
|   import { type Writable } from "svelte/store"; | ||||
|   import { debounce } from "$lib/helpers"; | ||||
|   import { createKeyMap } from "$lib/helpers/createKeyMap"; | ||||
|   import { GraphState } from "./state.svelte"; | ||||
|  | ||||
|   const state = new GraphState(); | ||||
|   setContext("graphState", state); | ||||
|   const graphState = new GraphState(); | ||||
|   setContext("graphState", graphState); | ||||
|  | ||||
|   type Props = { | ||||
|     graph: Graph; | ||||
|     registry: NodeRegistry; | ||||
|  | ||||
|     settings?: Writable<Record<string, any>>; | ||||
|     settings?: Record<string, any>; | ||||
|  | ||||
|     activeNode?: Node; | ||||
|     showGrid?: boolean; | ||||
| @@ -41,33 +40,32 @@ | ||||
|   }: Props = $props(); | ||||
|  | ||||
|   export const keymap = createKeyMap([]); | ||||
|   setContext("keymap", keymap); | ||||
|  | ||||
|   export const manager = new GraphManager(registry); | ||||
|   setContext("graphManager", manager); | ||||
|  | ||||
|   $effect(() => { | ||||
|     if (state.activeNodeId !== -1) { | ||||
|       activeNode = manager.getNode(state.activeNodeId); | ||||
|     if (graphState.activeNodeId !== -1) { | ||||
|       activeNode = manager.getNode(graphState.activeNodeId); | ||||
|     } else { | ||||
|       activeNode = undefined; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   setContext("keymap", keymap); | ||||
|  | ||||
|   const updateSettings = debounce((s) => { | ||||
|     manager.setSettings(s); | ||||
|   }, 200); | ||||
|  | ||||
|   $effect(() => { | ||||
|     if (settingTypes && settings) { | ||||
|       updateSettings($settings); | ||||
|       updateSettings($state.snapshot(settings)); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   manager.on("settings", (_settings) => { | ||||
|     settingTypes = _settings.types; | ||||
|     settings?.set(_settings.values); | ||||
|     settings = _settings.values; | ||||
|   }); | ||||
|  | ||||
|   manager.on("result", (result) => onresult?.(result)); | ||||
|   | ||||
							
								
								
									
										31
									
								
								app/src/lib/graph-interface/graph/colors.svelte.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/src/lib/graph-interface/graph/colors.svelte.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { appSettings } from "$lib/settings/app-settings.svelte"; | ||||
| import { Color } from "three"; | ||||
|  | ||||
| const variables = [ | ||||
|   "layer-0", | ||||
|   "layer-1", | ||||
|   "layer-2", | ||||
|   "layer-3", | ||||
|   "outline", | ||||
|   "active", | ||||
|   "selected", | ||||
|   "edge", | ||||
| ] as const; | ||||
|  | ||||
| function getColor(variable: typeof variables[number]) { | ||||
|   const style = getComputedStyle(document.body.parentElement!); | ||||
|   let color = style.getPropertyValue(`--${variable}`); | ||||
|   return new Color().setStyle(color); | ||||
| } | ||||
|  | ||||
| export const colors = Object.fromEntries(variables.map(v => [v, getColor(v)])) as Record<typeof variables[number], Color>; | ||||
|  | ||||
| $effect.root(() => { | ||||
|   $effect(() => { | ||||
|     if (!appSettings.theme || !("getComputedStyle" in globalThis)) return; | ||||
|     const style = getComputedStyle(document.body.parentElement!); | ||||
|     for (const v of variables) { | ||||
|       colors[v].setStyle(style.getPropertyValue(`--${v}`)); | ||||
|     } | ||||
|   }); | ||||
| }) | ||||
| @@ -1,48 +0,0 @@ | ||||
| import { readable } from "svelte/store"; | ||||
| import { Color } from "three"; | ||||
|  | ||||
| const variables = [ | ||||
|   "layer-0", | ||||
|   "layer-1", | ||||
|   "layer-2", | ||||
|   "layer-3", | ||||
|   "outline", | ||||
|   "active", | ||||
|   "selected", | ||||
|   "edge", | ||||
| ] as const; | ||||
|  | ||||
| const store = Object.fromEntries(variables.map(v => [v, new Color()])) as Record<typeof variables[number], Color>; | ||||
|  | ||||
| let lastStyle = ""; | ||||
|  | ||||
| function updateColors() { | ||||
|   if (!("getComputedStyle" in globalThis)) return; | ||||
|   const style = getComputedStyle(document.body.parentElement!); | ||||
|   let hash = ""; | ||||
|   for (const v of variables) { | ||||
|     let color = style.getPropertyValue(`--${v}`); | ||||
|     hash += color; | ||||
|     store[v].setStyle(color); | ||||
|   } | ||||
|   if (hash === lastStyle) return; | ||||
|   lastStyle = hash; | ||||
| } | ||||
|  | ||||
| export const colors = readable(store, set => { | ||||
|  | ||||
|   updateColors(); | ||||
|   set(store); | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     updateColors(); | ||||
|     set(store); | ||||
|   }, 1000); | ||||
|  | ||||
|   window.onload = function () { updateColors(); set(store) }; | ||||
|  | ||||
|   document.body.addEventListener("transitionstart", () => { | ||||
|     updateColors(); | ||||
|     set(store); | ||||
|   }) | ||||
| }); | ||||
| @@ -1,26 +1,22 @@ | ||||
| import type { Socket } from "@nodes/types"; | ||||
| import { getContext } from "svelte"; | ||||
| import { SvelteSet } from 'svelte/reactivity'; | ||||
|  | ||||
| export function getGraphState() { | ||||
|   return getContext<GraphState>("graphState"); | ||||
| } | ||||
|  | ||||
| export class GraphState { | ||||
|  | ||||
|   activeNodeId = $state(-1); | ||||
|   selectedNodes = $state(new Set<number>()); | ||||
|   selectedNodes = new SvelteSet<number>(); | ||||
|   activeSocket = $state<Socket | null>(null); | ||||
|   hoveredSocket = $state<Socket | null>(null); | ||||
|   possibleSockets = $state<Socket[]>([]); | ||||
|   possibleSocketIds = $derived(new Set( | ||||
|     this.possibleSockets.map((s) => `${s.node.id}-${s.index}`), | ||||
|   )); | ||||
|  | ||||
|   clearSelection() { | ||||
|     this.selectedNodes = new Set(); | ||||
|     this.selectedNodes.clear(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export { colors } from "./colors"; | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| <script lang="ts"> | ||||
|   import type { Node } from "@nodes/types"; | ||||
|   import { getContext, onMount } from "svelte"; | ||||
|   import { colors, getGraphState } from "../graph/state.svelte"; | ||||
|   import { getGraphState } from "../graph/state.svelte"; | ||||
|   import { T } from "@threlte/core"; | ||||
|   import { Color, type Mesh } from "three"; | ||||
|   import { type Mesh } from "three"; | ||||
|   import NodeFrag from "./Node.frag"; | ||||
|   import NodeVert from "./Node.vert"; | ||||
|   import NodeHtml from "./NodeHTML.svelte"; | ||||
|   import { colors } from "../graph/colors.svelte"; | ||||
|   import { appSettings } from "$lib/settings/app-settings.svelte"; | ||||
|  | ||||
|   const graphState = getGraphState(); | ||||
|  | ||||
| @@ -18,7 +20,16 @@ | ||||
|   const { node, inView, z }: Props = $props(); | ||||
|  | ||||
|   const isActive = $derived(graphState.activeNodeId === node.id); | ||||
|   const isSelected = $derived(!!graphState.selectedNodes?.has(node.id)); | ||||
|   const isSelected = $derived(graphState.selectedNodes.has(node.id)); | ||||
|   let strokeColor = $state(colors.selected); | ||||
|   $effect(() => { | ||||
|     appSettings.theme; | ||||
|     strokeColor = isSelected | ||||
|       ? colors.selected.clone() | ||||
|       : isActive | ||||
|         ? colors.active.clone() | ||||
|         : colors.outline.clone(); | ||||
|   }); | ||||
|  | ||||
|   const updateNodePosition = | ||||
|     getContext<(n: Node) => void>("updateNodePosition"); | ||||
| @@ -30,16 +41,11 @@ | ||||
|   const height = getNodeHeight?.(node.type); | ||||
|  | ||||
|   $effect(() => { | ||||
|     if (node && meshRef) { | ||||
|       node.tmp = node.tmp || {}; | ||||
|       node.tmp.mesh = meshRef; | ||||
|       updateNodePosition?.(node); | ||||
|     } | ||||
|     node.tmp = node.tmp || {}; | ||||
|     node.tmp.mesh = meshRef; | ||||
|     updateNodePosition?.(node); | ||||
|   }); | ||||
|  | ||||
|   const colorBright = $colors["layer-2"]; | ||||
|   const colorDark = $colors["layer-1"]; | ||||
|  | ||||
|   onMount(() => { | ||||
|     node.tmp = node.tmp || {}; | ||||
|     node.tmp.mesh = meshRef; | ||||
| @@ -61,20 +67,14 @@ | ||||
|     fragmentShader={NodeFrag} | ||||
|     transparent | ||||
|     uniforms={{ | ||||
|       uColorBright: { value: new Color("#171717") }, | ||||
|       uColorDark: { value: new Color("#151515") }, | ||||
|       uStrokeColor: { value: new Color("#9d5f28") }, | ||||
|       uColorBright: { value: colors["layer-2"] }, | ||||
|       uColorDark: { value: colors["layer-1"] }, | ||||
|       uStrokeColor: { value: colors.outline.clone() }, | ||||
|       uStrokeWidth: { value: 1.0 }, | ||||
|       uWidth: { value: 20 }, | ||||
|       uHeight: { value: height }, | ||||
|     }} | ||||
|     uniforms.uColorBright.value={colorBright} | ||||
|     uniforms.uColorDark.value={colorDark} | ||||
|     uniforms.uStrokeColor.value={isSelected | ||||
|       ? $colors.selected | ||||
|       : isActive | ||||
|         ? $colors.active | ||||
|         : $colors.outline} | ||||
|     uniforms.uStrokeColor.value={strokeColor.clone()} | ||||
|     uniforms.uStrokeWidth.value={(7 - z) / 3} | ||||
|   /> | ||||
| </T.Mesh> | ||||
|   | ||||
| @@ -3,14 +3,27 @@ | ||||
|   import NodeHeader from "./NodeHeader.svelte"; | ||||
|   import NodeParameter from "./NodeParameter.svelte"; | ||||
|   import { getContext, onMount } from "svelte"; | ||||
|   export let isActive = false; | ||||
|   export let isSelected = false; | ||||
|   export let inView = true; | ||||
|   export let z = 2; | ||||
|  | ||||
|   let ref: HTMLDivElement; | ||||
|   export let node: Node; | ||||
|   export let position = "absolute"; | ||||
|  | ||||
|   type Props = { | ||||
|     node: Node; | ||||
|  | ||||
|     position?: "absolute" | "fixed"; | ||||
|     isActive?: boolean; | ||||
|     isSelected?: boolean; | ||||
|     inView?: boolean; | ||||
|     z?: number; | ||||
|   }; | ||||
|  | ||||
|   let { | ||||
|     node, | ||||
|     position = "absolute", | ||||
|     isActive = false, | ||||
|     isSelected = false, | ||||
|     inView = true, | ||||
|     z = 2, | ||||
|   }: Props = $props(); | ||||
|  | ||||
|   const zOffset = (node.tmp?.random || 0) * 0.5; | ||||
|   const zLimit = 2 - zOffset; | ||||
| @@ -25,12 +38,6 @@ | ||||
|   const updateNodePosition = | ||||
|     getContext<(n: Node) => void>("updateNodePosition"); | ||||
|  | ||||
|   $: if (node && ref) { | ||||
|     node.tmp = node.tmp || {}; | ||||
|     node.tmp.ref = ref; | ||||
|     updateNodePosition?.(node); | ||||
|   } | ||||
|  | ||||
|   onMount(() => { | ||||
|     node.tmp = node.tmp || {}; | ||||
|     node.tmp.ref = ref; | ||||
|   | ||||
| @@ -26,9 +26,9 @@ | ||||
|   const aspectRatio = 0.25; | ||||
|  | ||||
|   const path = createNodePath({ | ||||
|     depth: 5, | ||||
|     height: 29, | ||||
|     y: 50, | ||||
|     depth: 5.5, | ||||
|     height: 34, | ||||
|     y: 49, | ||||
|     cornerTop, | ||||
|     rightBump, | ||||
|     aspectRatio, | ||||
| @@ -42,9 +42,9 @@ | ||||
|     aspectRatio, | ||||
|   }); | ||||
|   const pathHover = createNodePath({ | ||||
|     depth: 9, | ||||
|     depth: 8.5, | ||||
|     height: 50, | ||||
|     y: 50, | ||||
|     y: 49, | ||||
|     cornerTop, | ||||
|     rightBump, | ||||
|     aspectRatio, | ||||
| @@ -103,12 +103,12 @@ | ||||
|  | ||||
|   svg { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     top: 1px; | ||||
|     left: 1px; | ||||
|     z-index: -1; | ||||
|     box-sizing: border-box; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     width: calc(100% - 2px); | ||||
|     height: calc(100% - 1px); | ||||
|     overflow: visible; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -53,14 +53,14 @@ | ||||
|   const path = createNodePath({ | ||||
|     depth: 7, | ||||
|     height: 20, | ||||
|     y: 51, | ||||
|     y: 50.5, | ||||
|     cornerBottom, | ||||
|     leftBump, | ||||
|     aspectRatio, | ||||
|   }); | ||||
|   const pathDisabled = createNodePath({ | ||||
|     depth: 4.5, | ||||
|     height: 14, | ||||
|     depth: 6, | ||||
|     height: 18, | ||||
|     y: 50.5, | ||||
|     cornerBottom, | ||||
|     leftBump, | ||||
| @@ -172,11 +172,11 @@ | ||||
|   svg { | ||||
|     position: absolute; | ||||
|     box-sizing: border-box; | ||||
|     width: 100%; | ||||
|     width: calc(100% - 2px); | ||||
|     height: 100%; | ||||
|     overflow: visible; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     left: 1px; | ||||
|     z-index: -1; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -18,7 +18,6 @@ export function createKeyMap(keys: Shortcut[]) { | ||||
|  | ||||
|   const store = writable(new Map(keys.map(k => [getShortcutId(k), k]))); | ||||
|  | ||||
|  | ||||
|   return { | ||||
|     handleKeyboardEvent: (event: KeyboardEvent) => { | ||||
|       const activeElement = document.activeElement as HTMLElement; | ||||
|   | ||||
| @@ -11,9 +11,9 @@ | ||||
|     fps: false, | ||||
|   }); | ||||
|  | ||||
|   $: vertices = $store?.at(-1)?.["total-vertices"][0] || 0; | ||||
|   $: faces = $store?.at(-1)?.["total-faces"][0] || 0; | ||||
|   $: runtime = $store?.at(-1)?.["runtime"][0] || 0; | ||||
|   $: vertices = $store?.at(-1)?.["total-vertices"]?.[0] || 0; | ||||
|   $: faces = $store?.at(-1)?.["total-faces"]?.[0] || 0; | ||||
|   $: runtime = $store?.at(-1)?.["runtime"]?.[0] || 0; | ||||
|  | ||||
|   function getPoints(data: PerformanceData, key: string) { | ||||
|     return data?.map((run) => run[key]?.[0] || 0) || []; | ||||
|   | ||||
| @@ -9,21 +9,28 @@ | ||||
|     Box3, | ||||
|     Mesh, | ||||
|     MeshBasicMaterial, | ||||
|     Color, | ||||
|   } from "three"; | ||||
|   import { AppSettings } from "../settings/app-settings"; | ||||
|   import { appSettings } from "../settings/app-settings.svelte"; | ||||
|   import Camera from "./Camera.svelte"; | ||||
|   import { colors } from "$lib/graph-interface/graph/colors.svelte"; | ||||
|  | ||||
|   const { renderStage, invalidate: _invalidate } = useThrelte(); | ||||
|  | ||||
|   export let fps: number[] = []; | ||||
|   // let renderer = threlte.renderer; | ||||
|   // let rendererRender = renderer.render; | ||||
|   // renderer.render = function (scene, camera) { | ||||
|   //   const a = performance.now(); | ||||
|   //   rendererRender.call(renderer, scene, camera); | ||||
|   //   fps.push(performance.now() - a); | ||||
|   //   fps = fps.slice(-100); | ||||
|   // }; | ||||
|   type Props = { | ||||
|     fps: number[]; | ||||
|     lines: Vector3[][]; | ||||
|     scene: Group; | ||||
|     centerCamera: boolean; | ||||
|   }; | ||||
|  | ||||
|   let { | ||||
|     lines, | ||||
|     centerCamera, | ||||
|     fps = $bindable(), | ||||
|     scene = $bindable(), | ||||
|   }: Props = $props(); | ||||
|  | ||||
|   useTask( | ||||
|     (delta) => { | ||||
|       fps.push(1 / delta); | ||||
| @@ -53,12 +60,8 @@ | ||||
|     _invalidate(); | ||||
|   }; | ||||
|  | ||||
|   let geometries: BufferGeometry[] = []; | ||||
|   export let lines: Vector3[][]; | ||||
|   export let scene: Group; | ||||
|  | ||||
|   export let centerCamera: boolean = true; | ||||
|   let center = new Vector3(0, 4, 0); | ||||
|   let geometries = $state<BufferGeometry[]>(); | ||||
|   let center = $state(new Vector3(0, 4, 0)); | ||||
|  | ||||
|   function isMesh(child: Mesh | any): child is Mesh { | ||||
|     return child.isObject3D && "material" in child; | ||||
| @@ -68,14 +71,15 @@ | ||||
|     return material.isMaterial && "matcap" in material; | ||||
|   } | ||||
|  | ||||
|   $: if ($AppSettings && scene) { | ||||
|   $effect(() => { | ||||
|     const wireframe = appSettings.debug.wireframe; | ||||
|     scene.traverse(function (child) { | ||||
|       if (isMesh(child) && isMatCapMaterial(child.material)) { | ||||
|         child.material.wireframe = $AppSettings.wireframe; | ||||
|         child.material.wireframe = wireframe; | ||||
|       } | ||||
|     }); | ||||
|     invalidate(); | ||||
|   } | ||||
|     _invalidate(); | ||||
|   }); | ||||
|  | ||||
|   function getPosition(geo: BufferGeometry, i: number) { | ||||
|     return [ | ||||
| @@ -88,32 +92,38 @@ | ||||
|  | ||||
| <Camera {center} {centerCamera} /> | ||||
|  | ||||
| {#if $AppSettings.showGrid} | ||||
|   <T.GridHelper args={[20, 20]} /> | ||||
| {#if appSettings.showGrid} | ||||
|   <T.GridHelper | ||||
|     args={[20, 20]} | ||||
|     colorGrid={colors["outline"]} | ||||
|     colorCenterLine={new Color("red")} | ||||
|   /> | ||||
| {/if} | ||||
|  | ||||
| <T.Group> | ||||
|   {#each geometries as geo} | ||||
|     {#if $AppSettings.showIndices} | ||||
|       {#each geo.attributes.position.array as _, i} | ||||
|         {#if i % 3 === 0} | ||||
|           <Text fontSize={0.25} position={getPosition(geo, i)} /> | ||||
|         {/if} | ||||
|       {/each} | ||||
|     {/if} | ||||
|   {#if geometries} | ||||
|     {#each geometries as geo} | ||||
|       {#if appSettings.debug.showIndices} | ||||
|         {#each geo.attributes.position.array as _, i} | ||||
|           {#if i % 3 === 0} | ||||
|             <Text fontSize={0.25} position={getPosition(geo, i)} /> | ||||
|           {/if} | ||||
|         {/each} | ||||
|       {/if} | ||||
|  | ||||
|     {#if $AppSettings.showVertices} | ||||
|       <T.Points visible={true}> | ||||
|         <T is={geo} /> | ||||
|         <T.PointsMaterial size={0.25} /> | ||||
|       </T.Points> | ||||
|     {/if} | ||||
|   {/each} | ||||
|       {#if appSettings.debug.showVertices} | ||||
|         <T.Points visible={true}> | ||||
|           <T is={geo} /> | ||||
|           <T.PointsMaterial size={0.25} /> | ||||
|         </T.Points> | ||||
|       {/if} | ||||
|     {/each} | ||||
|   {/if} | ||||
|  | ||||
|   <T.Group bind:ref={scene}></T.Group> | ||||
| </T.Group> | ||||
|  | ||||
| {#if $AppSettings.showStemLines && lines} | ||||
| {#if appSettings.debug.showStemLines && lines} | ||||
|   {#each lines as line} | ||||
|     <T.Mesh> | ||||
|       <MeshLineGeometry points={line} /> | ||||
|   | ||||
| @@ -2,12 +2,10 @@ | ||||
|   import { Canvas } from "@threlte/core"; | ||||
|   import Scene from "./Scene.svelte"; | ||||
|   import { Vector3 } from "three"; | ||||
|  | ||||
|   import { decodeFloat, splitNestedArray } from "@nodes/utils"; | ||||
|   import type { PerformanceStore } from "@nodes/utils"; | ||||
|   import { AppSettings } from "$lib/settings/app-settings"; | ||||
|   import { appSettings } from "$lib/settings/app-settings.svelte"; | ||||
|   import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte"; | ||||
|  | ||||
|   import { MeshMatcapMaterial, TextureLoader, type Group } from "three"; | ||||
|   import { | ||||
|     createGeometryPool, | ||||
| @@ -22,9 +20,11 @@ | ||||
|     matcap, | ||||
|   }); | ||||
|  | ||||
|   let sceneComponent = $state<ReturnType<typeof Scene>>(); | ||||
|   let fps = $state<number[]>([]); | ||||
|  | ||||
|   let geometryPool: ReturnType<typeof createGeometryPool>; | ||||
|   let instancePool: ReturnType<typeof createInstancedGeometryPool>; | ||||
|  | ||||
|   export function updateGeometries(inputs: Int32Array[], group: Group) { | ||||
|     geometryPool = geometryPool || createGeometryPool(group, material); | ||||
|     instancePool = instancePool || createInstancedGeometryPool(group, material); | ||||
| @@ -38,14 +38,15 @@ | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   export let centerCamera: boolean = true; | ||||
|   export let perf: PerformanceStore; | ||||
|   export let scene: Group; | ||||
|   let fps: number[] = []; | ||||
|   type Props = { | ||||
|     scene: Group; | ||||
|     centerCamera: boolean; | ||||
|     perf: PerformanceStore; | ||||
|   }; | ||||
|  | ||||
|   let lines: Vector3[][] = []; | ||||
|   let { scene = $bindable(), centerCamera, perf }: Props = $props(); | ||||
|  | ||||
|   let invalidate: () => void; | ||||
|   let lines = $state<Vector3[][]>([]); | ||||
|  | ||||
|   function createLineGeometryFromEncodedData(encodedData: Int32Array) { | ||||
|     const positions: Vector3[] = []; | ||||
| @@ -63,12 +64,12 @@ | ||||
|   } | ||||
|  | ||||
|   export const update = function update(result: Int32Array) { | ||||
|     perf?.addPoint("split-result"); | ||||
|     perf.addPoint("split-result"); | ||||
|     const inputs = splitNestedArray(result); | ||||
|     perf?.endPoint(); | ||||
|     perf.endPoint(); | ||||
|  | ||||
|     if ($AppSettings.showStemLines) { | ||||
|       perf?.addPoint("create-lines"); | ||||
|     if (appSettings.debug.showStemLines) { | ||||
|       perf.addPoint("create-lines"); | ||||
|       lines = inputs | ||||
|         .map((input) => { | ||||
|           if (input[0] === 0) { | ||||
| @@ -79,21 +80,27 @@ | ||||
|       perf.endPoint(); | ||||
|     } | ||||
|  | ||||
|     perf?.addPoint("update-geometries"); | ||||
|     perf.addPoint("update-geometries"); | ||||
|  | ||||
|     const { totalVertices, totalFaces } = updateGeometries(inputs, scene); | ||||
|     perf?.endPoint(); | ||||
|     perf.endPoint(); | ||||
|  | ||||
|     perf?.addPoint("total-vertices", totalVertices); | ||||
|     perf?.addPoint("total-faces", totalFaces); | ||||
|     invalidate(); | ||||
|     perf.addPoint("total-vertices", totalVertices); | ||||
|     perf.addPoint("total-faces", totalFaces); | ||||
|     sceneComponent?.invalidate(); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| {#if $AppSettings.showPerformancePanel} | ||||
| {#if appSettings.debug.showPerformancePanel} | ||||
|   <SmallPerformanceViewer {fps} store={perf} /> | ||||
| {/if} | ||||
|  | ||||
| <Canvas> | ||||
|   <Scene bind:scene bind:invalidate {lines} {centerCamera} bind:fps /> | ||||
|   <Scene | ||||
|     bind:this={sceneComponent} | ||||
|     {lines} | ||||
|     {centerCamera} | ||||
|     bind:scene | ||||
|     bind:fps | ||||
|   /> | ||||
| </Canvas> | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { fastHashArrayBuffer } from "@nodes/utils"; | ||||
| import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three" | ||||
|  | ||||
| import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three"; | ||||
|  | ||||
| function fastArrayHash(arr: ArrayBuffer) { | ||||
|   let ints = new Uint8Array(arr); | ||||
| @@ -108,7 +107,6 @@ export function createGeometryPool(parentScene: Group, material: Material) { | ||||
|       scene.add(mesh); | ||||
|       meshes.push(mesh); | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor, NodeInput } from "@nodes/types"; | ||||
| import { concatEncodedArrays, encodeFloat, fastHashArrayBuffer, createLogger, type PerformanceStore } from "@nodes/utils" | ||||
| import type { SyncCache } from "@nodes/types"; | ||||
| import type { Graph, NodeDefinition, NodeInput, NodeRegistry, RuntimeExecutor, SyncCache } from "@nodes/types"; | ||||
| import { concatEncodedArrays, createLogger, encodeFloat, fastHashArrayBuffer, type PerformanceStore } from "@nodes/utils"; | ||||
|  | ||||
| const log = createLogger("runtime-executor"); | ||||
| log.mute() | ||||
| @@ -9,6 +8,7 @@ function getValue(input: NodeInput, value?: unknown) { | ||||
|   if (value === undefined && "value" in input) { | ||||
|     value = input.value | ||||
|   } | ||||
|  | ||||
|   if (input.type === "float") { | ||||
|     return encodeFloat(value as number); | ||||
|   } | ||||
|   | ||||
| @@ -5,9 +5,6 @@ import type { Graph, RuntimeExecutor } from "@nodes/types"; | ||||
| export class WorkerRuntimeExecutor implements RuntimeExecutor { | ||||
|   private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)); | ||||
|  | ||||
|   constructor() { | ||||
|     console.log(import.meta.url) | ||||
|   } | ||||
|   async execute(graph: Graph, settings: Record<string, unknown>) { | ||||
|     return this.worker.executeGraph(graph, settings); | ||||
|   } | ||||
|   | ||||
| @@ -39,6 +39,7 @@ export const AppSettingTypes = { | ||||
|     } | ||||
|   }, | ||||
|   debug: { | ||||
|     title: "Debug", | ||||
|     wireframe: { | ||||
|       type: "boolean", | ||||
|       label: "Wireframe", | ||||
| @@ -79,7 +80,8 @@ export const AppSettingTypes = { | ||||
|       amount: { | ||||
|         type: "integer", | ||||
|         min: 2, | ||||
|         max: 15 | ||||
|         max: 15, | ||||
|         value: 4 | ||||
|       }, | ||||
|       loadGrid: { | ||||
|         type: "button", | ||||
| @@ -138,8 +140,8 @@ export const appSettings = localState("app-settings", settingsToStore(AppSetting | ||||
|  | ||||
| $effect.root(() => { | ||||
|   $effect(() => { | ||||
|     const { theme } = $state.snapshot(appSettings); | ||||
|     const classes = document.body.parentElement?.classList; | ||||
|     const theme = appSettings.theme; | ||||
|     const classes = document.documentElement.classList; | ||||
|     const newClassName = `theme-${theme}`; | ||||
|     if (classes) { | ||||
|       for (const className of classes) { | ||||
| @@ -148,6 +150,6 @@ $effect.root(() => { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     document.body?.parentElement?.classList.add(newClassName); | ||||
|     document.documentElement.classList.add(newClassName); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,136 +0,0 @@ | ||||
| import localStore from "$lib/helpers/localStore"; | ||||
|  | ||||
| export const AppSettings = localStore("node.settings", { | ||||
|   theme: 0, | ||||
|   showGrid: true, | ||||
|   showNodeGrid: true, | ||||
|   snapToGrid: true, | ||||
|   showHelp: false, | ||||
|   wireframe: false, | ||||
|   showIndices: false, | ||||
|   showVertices: false, | ||||
|   showPerformancePanel: false, | ||||
|   showBenchmarkPanel: false, | ||||
|   centerCamera: true, | ||||
|   showStemLines: false, | ||||
|   useWorker: true, | ||||
|   amount: 5 | ||||
| }); | ||||
|  | ||||
| const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"]; | ||||
|  | ||||
| AppSettings.subscribe((value) => { | ||||
|   const classes = document.body.parentElement?.classList; | ||||
|   const newClassName = `theme-${themes[value.theme]}`; | ||||
|   if (classes) { | ||||
|     for (const className of classes) { | ||||
|       if (className.startsWith("theme-") && className !== newClassName) { | ||||
|         classes.remove(className); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   document.body?.parentElement?.classList.add(newClassName); | ||||
| }); | ||||
|  | ||||
| export const AppSettingTypes = { | ||||
|   theme: { | ||||
|     type: "select", | ||||
|     options: themes, | ||||
|     label: "Theme", | ||||
|     value: themes[0], | ||||
|   }, | ||||
|   showGrid: { | ||||
|     type: "boolean", | ||||
|     label: "Show Grid", | ||||
|     value: true, | ||||
|   }, | ||||
|   centerCamera: { | ||||
|     type: "boolean", | ||||
|     label: "Center Camera", | ||||
|     value: true | ||||
|   }, | ||||
|   nodeInterface: { | ||||
|     __title: "Node Interface", | ||||
|     showNodeGrid: { | ||||
|       type: "boolean", | ||||
|       label: "Show Grid", | ||||
|       value: true | ||||
|     }, | ||||
|     snapToGrid: { | ||||
|       type: "boolean", | ||||
|       label: "Snap to Grid", | ||||
|       value: true | ||||
|     }, | ||||
|     showHelp: { | ||||
|       type: "boolean", | ||||
|       label: "Show Help", | ||||
|       value: false | ||||
|     } | ||||
|   }, | ||||
|   debug: { | ||||
|     wireframe: { | ||||
|       type: "boolean", | ||||
|       label: "Wireframe", | ||||
|       value: false, | ||||
|     }, | ||||
|     useWorker: { | ||||
|       type: "boolean", | ||||
|       label: "Execute runtime in worker", | ||||
|       value: true, | ||||
|     }, | ||||
|     showIndices: { | ||||
|       type: "boolean", | ||||
|       label: "Show Indices", | ||||
|       value: false, | ||||
|     }, | ||||
|     showPerformancePanel: { | ||||
|       type: "boolean", | ||||
|       label: "Show Performance Panel", | ||||
|       value: false, | ||||
|     }, | ||||
|     showBenchmarkPanel: { | ||||
|       type: "boolean", | ||||
|       label: "Show Benchmark Panel", | ||||
|       value: false, | ||||
|     }, | ||||
|     showVertices: { | ||||
|       type: "boolean", | ||||
|       label: "Show Vertices", | ||||
|       value: false, | ||||
|     }, | ||||
|     showStemLines: { | ||||
|       type: "boolean", | ||||
|       label: "Show Stem Lines", | ||||
|       value: false, | ||||
|     }, | ||||
|     stressTest: { | ||||
|       __title: "Stress Test", | ||||
|       amount: { | ||||
|         type: "integer", | ||||
|         min: 2, | ||||
|         max: 15 | ||||
|       }, | ||||
|       loadGrid: { | ||||
|         type: "button", | ||||
|         label: "Load Grid" | ||||
|       }, | ||||
|       loadTree: { | ||||
|         type: "button", | ||||
|         label: "Load Tree" | ||||
|       }, | ||||
|       lottaFaces: { | ||||
|         type: "button", | ||||
|         label: "Load 'lots of faces'" | ||||
|       }, | ||||
|       lottaNodes: { | ||||
|         type: "button", | ||||
|         label: "Load 'lots of nodes'" | ||||
|       }, | ||||
|       lottaNodesAndFaces: { | ||||
|         type: "button", | ||||
|         label: "Load 'lots of nodes and faces'" | ||||
|       } | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										92
									
								
								app/src/lib/settings/panels/ActiveNodeSelected.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/src/lib/settings/panels/ActiveNodeSelected.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| <script lang="ts"> | ||||
|   import type { Node, NodeInput } from "@nodes/types"; | ||||
|   import NestedSettings from "./NestedSettings.svelte"; | ||||
|   import type { GraphManager } from "$lib/graph-interface/graph-manager"; | ||||
|  | ||||
|   type Props = { | ||||
|     manager: GraphManager; | ||||
|     node: Node; | ||||
|   }; | ||||
|  | ||||
|   const { manager, node }: Props = $props(); | ||||
|  | ||||
|   const nodeDefinition = filterInputs(node.tmp?.type?.inputs); | ||||
|   function filterInputs(inputs?: Record<string, NodeInput>) { | ||||
|     const _inputs = $state.snapshot(inputs); | ||||
|     return Object.fromEntries( | ||||
|       Object.entries(structuredClone(_inputs ?? {})) | ||||
|         .filter(([_key, value]) => { | ||||
|           return value.hidden === true; | ||||
|         }) | ||||
|         .map(([key, value]) => { | ||||
|           //@ts-ignore | ||||
|           value.__node_type = node?.tmp?.type.id; | ||||
|           //@ts-ignore | ||||
|           value.__node_input = key; | ||||
|           return [key, value]; | ||||
|         }), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   type Store = Record<string, number | number[]>; | ||||
|   let store = $state<Store>(createStore(node?.props, nodeDefinition)); | ||||
|   function createStore( | ||||
|     props: Node["props"], | ||||
|     inputs: Record<string, NodeInput>, | ||||
|   ): Store { | ||||
|     const store: Store = {}; | ||||
|     Object.keys(inputs).forEach((key) => { | ||||
|       if (props) { | ||||
|         //@ts-ignore | ||||
|         store[key] = props[key] || inputs[key].value; | ||||
|       } | ||||
|     }); | ||||
|     return store; | ||||
|   } | ||||
|  | ||||
|   let lastPropsHash = ""; | ||||
|   function updateNode() { | ||||
|     if (!node || !store) return; | ||||
|     let needsUpdate = false; | ||||
|     Object.keys(store).forEach((_key: string) => { | ||||
|       node.props = node.props || {}; | ||||
|       const key = _key as keyof typeof store; | ||||
|       if (node && store) { | ||||
|         needsUpdate = true; | ||||
|         node.props[key] = store[key]; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     let propsHash = JSON.stringify(node.props); | ||||
|     if (propsHash === lastPropsHash) { | ||||
|       return; | ||||
|     } | ||||
|     lastPropsHash = propsHash; | ||||
|  | ||||
|     if (needsUpdate) { | ||||
|       manager.execute(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   $effect(() => { | ||||
|     if (store && store) { | ||||
|       updateNode(); | ||||
|     } | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| {#if node} | ||||
|   {#key node.id} | ||||
|     {#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0} | ||||
|       <NestedSettings | ||||
|         id="activeNodeSettings" | ||||
|         bind:value={store} | ||||
|         type={nodeDefinition} | ||||
|       /> | ||||
|     {:else} | ||||
|       <p class="mx-4">Active Node has no Settings</p> | ||||
|     {/if} | ||||
|   {/key} | ||||
| {:else} | ||||
|   <p class="mx-4">No active node</p> | ||||
| {/if} | ||||
| @@ -1,85 +1,20 @@ | ||||
| <script lang="ts"> | ||||
|   import type { Node, NodeInput } from "@nodes/types"; | ||||
|   import NestedSettings from "./NestedSettings.svelte"; | ||||
|   import { writable } from "svelte/store"; | ||||
|  | ||||
|   import type { Node } from "@nodes/types"; | ||||
|   import type { GraphManager } from "$lib/graph-interface/graph-manager"; | ||||
|   import ActiveNodeSelected from "./ActiveNodeSelected.svelte"; | ||||
|  | ||||
|   export let manager: GraphManager; | ||||
|   export let node: Node | undefined; | ||||
|   type Props = { | ||||
|     manager: GraphManager; | ||||
|     node: Node | undefined; | ||||
|   }; | ||||
|  | ||||
|   function filterInputs(inputs: Record<string, NodeInput>) { | ||||
|     return Object.fromEntries( | ||||
|       Object.entries(inputs) | ||||
|         .filter(([_key, value]) => { | ||||
|           return value.hidden === true; | ||||
|         }) | ||||
|         .map(([key, value]) => { | ||||
|           //@ts-ignore | ||||
|           value.__node_type = node?.tmp?.type.id; | ||||
|           //@ts-ignore | ||||
|           value.__node_input = key; | ||||
|           return [key, value]; | ||||
|         }), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   function createStore( | ||||
|     props: Node["props"], | ||||
|     inputs: Record<string, NodeInput>, | ||||
|   ) { | ||||
|     const store: Record<string, unknown> = {}; | ||||
|     Object.keys(inputs).forEach((key) => { | ||||
|       if (props) { | ||||
|         //@ts-ignore | ||||
|         store[key] = props[key] || inputs[key].value; | ||||
|       } | ||||
|     }); | ||||
|     return writable(store); | ||||
|   } | ||||
|  | ||||
|   let nodeDefinition: Record<string, NodeInput> | undefined; | ||||
|   $: nodeDefinition = node?.tmp?.type | ||||
|     ? filterInputs(node.tmp.type.inputs) | ||||
|     : undefined; | ||||
|   $: store = node ? createStore(node.props, nodeDefinition) : undefined; | ||||
|  | ||||
|   let lastPropsHash = ""; | ||||
|   function updateNode() { | ||||
|     if (!node || !$store) return; | ||||
|     let needsUpdate = false; | ||||
|     Object.keys($store).forEach((_key: string) => { | ||||
|       node.props = node.props || {}; | ||||
|       const key = _key as keyof typeof $store; | ||||
|       if (node && $store) { | ||||
|         needsUpdate = true; | ||||
|         node.props[key] = $store[key]; | ||||
|       } | ||||
|     }); | ||||
|     let propsHash = JSON.stringify(node.props); | ||||
|     if (propsHash === lastPropsHash) { | ||||
|       return; | ||||
|     } | ||||
|     lastPropsHash = propsHash; | ||||
|     // console.log(needsUpdate, node.props, $store); | ||||
|     if (needsUpdate) { | ||||
|       manager.execute(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   $: if (store && $store) { | ||||
|     updateNode(); | ||||
|   } | ||||
|   const { manager, node }: Props = $props(); | ||||
| </script> | ||||
|  | ||||
| {#if node} | ||||
|   {#key node.id} | ||||
|     {#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0} | ||||
|       <NestedSettings | ||||
|         id="activeNodeSettings" | ||||
|         settings={nodeDefinition} | ||||
|         {store} | ||||
|       /> | ||||
|     {#if node} | ||||
|       <ActiveNodeSelected {manager} {node} /> | ||||
|     {:else} | ||||
|       <p class="mx-4">Active Node has no Settings</p> | ||||
|     {/if} | ||||
|   | ||||
| @@ -1,44 +1,49 @@ | ||||
| <script lang="ts"> | ||||
|   import type { createKeyMap } from "$lib/helpers/createKeyMap"; | ||||
|   import { ShortCut } from "@nodes/ui"; | ||||
|   import { get } from "svelte/store"; | ||||
|  | ||||
|   export let keymap: ReturnType<typeof createKeyMap>; | ||||
|   const keys = keymap?.keys; | ||||
|   export let title = "Keymap"; | ||||
|   type Props = { | ||||
|     keymaps: { | ||||
|       keymap: ReturnType<typeof createKeyMap>; | ||||
|       title: string; | ||||
|     }[]; | ||||
|   }; | ||||
|  | ||||
|   let { keymaps }: Props = $props(); | ||||
|   console.log({ keymaps }); | ||||
| </script> | ||||
|  | ||||
| <div class="wrapper"> | ||||
|   <h3>{title}</h3> | ||||
|  | ||||
|   <section> | ||||
|     {#each $keys as key} | ||||
|       {#if key.description} | ||||
|         <div class="command-wrapper"> | ||||
|           <ShortCut | ||||
|             alt={key.alt} | ||||
|             ctrl={key.ctrl} | ||||
|             shift={key.shift} | ||||
|             key={key.key} | ||||
|           /> | ||||
|         </div> | ||||
|         <p>{key.description}</p> | ||||
|       {/if} | ||||
| <table class="wrapper"> | ||||
|   <tbody> | ||||
|     {#each keymaps as keymap} | ||||
|       <tr> | ||||
|         <td colspan="2"> | ||||
|           <h3>{keymap.title}</h3> | ||||
|         </td> | ||||
|       </tr> | ||||
|       {#each get(keymap.keymap?.keys) as key} | ||||
|         <tr> | ||||
|           {#if key.description} | ||||
|             <td class="command-wrapper"> | ||||
|               <ShortCut | ||||
|                 alt={key.alt} | ||||
|                 ctrl={key.ctrl} | ||||
|                 shift={key.shift} | ||||
|                 key={key.key} | ||||
|               /> | ||||
|             </td> | ||||
|             <td>{key.description}</td> | ||||
|           {/if} | ||||
|         </tr> | ||||
|       {/each} | ||||
|     {/each} | ||||
|   </section> | ||||
| </div> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <style> | ||||
|   .wrapper { | ||||
|     padding: 1em; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 1em; | ||||
|   } | ||||
|  | ||||
|   section { | ||||
|     display: grid; | ||||
|     grid-template-columns: min-content 1fr; | ||||
|     gap: 1em; | ||||
|   } | ||||
|  | ||||
|   h3 { | ||||
| @@ -51,10 +56,11 @@ | ||||
|     align-items: center; | ||||
|   } | ||||
|  | ||||
|   p { | ||||
|   td { | ||||
|     font-size: 0.9em; | ||||
|     margin: 0; | ||||
|     display: flex; | ||||
|     padding: 7px; | ||||
|     padding-left: 0; | ||||
|     align-items: center; | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -1,3 +1,7 @@ | ||||
| <script module lang="ts"> | ||||
|   let openSections = localState<Record<string,boolean>>("open-details", {}); | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
|   import NestedSettings from "./NestedSettings.svelte"; | ||||
|   import {localState} from "$lib/helpers/localState.svelte"; | ||||
| @@ -11,15 +15,18 @@ | ||||
|   interface Nested { | ||||
|     [key: string]: (Nested & { title?: string }) | InputType; | ||||
|   } | ||||
|   type SettingsType = Record<string, Nested>; | ||||
|   type SettingsValue = Record<string, Record<string, unknown> | string | number | boolean>; | ||||
|  | ||||
|   type Props = { | ||||
|     id: string; | ||||
|     key?: string; | ||||
|     value: Record<string, unknown> | string | number | boolean; | ||||
|     type: Nested; | ||||
|     value: SettingsValue; | ||||
|     type: SettingsType; | ||||
|     depth?: number; | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props(); | ||||
|  | ||||
|   function isNodeInput(v: InputType | Nested): v is InputType { | ||||
| @@ -28,14 +35,14 @@ | ||||
|  | ||||
|   let internalValue = $state(Array.isArray(type?.[key]?.options) ? type[key]?.options?.indexOf(value?.[key]) : value?.[key]); | ||||
|  | ||||
|   let openSections = localState("open-details", {}); | ||||
|   let open = $state(openSections[id]); | ||||
|   if(depth > 0 && !isNodeInput(type[key])){ | ||||
|     $effect(() => { | ||||
|       if(open !== undefined){} | ||||
|       openSections[id] = open; | ||||
|       }); | ||||
|     } | ||||
|       if(open !== undefined){ | ||||
|         openSections[id] = open; | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|  | ||||
|   $effect(() => { | ||||
| @@ -49,7 +56,7 @@ | ||||
| </script> | ||||
|  | ||||
| {#if key && isNodeInput(type?.[key]) } | ||||
|   <div class="input input-{type[key].type}"> | ||||
|   <div class="input input-{type[key].type}" class:first-level={depth === 1}> | ||||
|     {#if type[key].type === "button"} | ||||
|       <button onclick={() => console.log(type[key])}> | ||||
|         {type[key].label || key} | ||||
| @@ -65,27 +72,25 @@ | ||||
|       <NestedSettings | ||||
|         id={`${id}.${childKey}`} | ||||
|         key={childKey} | ||||
|         value={value as Record<string, unknown>} | ||||
|         type={type as Nested} | ||||
|         value={value} | ||||
|         type={type} | ||||
|         depth={depth + 1} | ||||
|       /> | ||||
|     {/each} | ||||
|   {#if depth > 0} | ||||
|     <hr /> | ||||
|   {/if} | ||||
|   {:else if key && type?.[key]} | ||||
|   {#if depth > 0} | ||||
|     <hr /> | ||||
|   {/if} | ||||
|     <details bind:open> | ||||
|       <summary>{type[key]?.title||key}</summary> | ||||
|       <summary><p>{type[key]?.title||key}</p></summary> | ||||
|       <div class="content"> | ||||
|         {#each Object.keys(type[key]).filter((key) => key !== "title") as childKey} | ||||
|           <NestedSettings | ||||
|             id={`${id}.${childKey}`} | ||||
|             key={childKey} | ||||
|             value={value[key] as Record<string, unknown>} | ||||
|             type={type[key] as Nested} | ||||
|             value={value[key] as SettingsValue} | ||||
|             type={type[key] as SettingsType} | ||||
|             depth={depth + 1} | ||||
|           /> | ||||
|         {/each} | ||||
| @@ -103,9 +108,18 @@ | ||||
|     user-select: none; | ||||
|     margin-bottom: 1em; | ||||
|   } | ||||
|  | ||||
|   summary::marker { } | ||||
|  | ||||
|   summary > p { | ||||
|     display: inline; | ||||
|     padding-left: 6px; | ||||
|   } | ||||
|  | ||||
|   details { | ||||
|     padding: 1em; | ||||
|     padding-bottom: 0; | ||||
|     padding-left: 21px; | ||||
|   } | ||||
|  | ||||
|   .input { | ||||
| @@ -114,7 +128,7 @@ | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 10px; | ||||
|     padding-left: 14px; | ||||
|     padding-left: 20px; | ||||
|   } | ||||
|  | ||||
|   .input-boolean { | ||||
| @@ -126,16 +140,12 @@ | ||||
|     order: 2; | ||||
|   } | ||||
|  | ||||
|   .first-level > .input { | ||||
|     padding-right: 1rem; | ||||
|   .first-level.input { | ||||
|     padding-left: 1em; | ||||
|     padding-right: 1em; | ||||
|     padding-bottom: 1px; | ||||
|   } | ||||
|  | ||||
|   .first-level { | ||||
|     border-bottom: solid thin var(--outline); | ||||
|   } | ||||
|   .first-level > details { | ||||
|     border: none; | ||||
|   } | ||||
|   hr { | ||||
|     position: absolute; | ||||
|     margin: 0; | ||||
|   | ||||
| @@ -5,9 +5,10 @@ | ||||
|   import type { Graph, Node } from "@nodes/types"; | ||||
|   import Viewer from "$lib/result-viewer/Viewer.svelte"; | ||||
|   import Settings from "$lib/settings/Settings.svelte"; | ||||
|   import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings"; | ||||
|   import { appSettings as _appSettings, AppSettingTypes as _AppSettingTypes} from "$lib/settings/app-settings.svelte"; | ||||
|   import { writable } from "svelte/store"; | ||||
|   import { | ||||
|     appSettings, | ||||
|     AppSettingTypes, | ||||
|   } from "$lib/settings/app-settings.svelte"; | ||||
|   import Keymap from "$lib/settings/panels/Keymap.svelte"; | ||||
|   import { createKeyMap } from "$lib/helpers/createKeyMap"; | ||||
|   import NodeStore from "$lib/node-store/NodeStore.svelte"; | ||||
| @@ -27,6 +28,7 @@ | ||||
|   import { createPerformanceStore } from "@nodes/utils"; | ||||
|   import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte"; | ||||
|   import { debounceAsyncFunction } from "$lib/helpers"; | ||||
|   import { onMount } from "svelte"; | ||||
|  | ||||
|   let performanceStore = createPerformanceStore(); | ||||
|  | ||||
| @@ -38,24 +40,26 @@ | ||||
|   const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); | ||||
|   memoryRuntime.perf = performanceStore; | ||||
|  | ||||
|   $: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime; | ||||
|   const runtime = $derived( | ||||
|     appSettings.debug.useWorker ? workerRuntime : memoryRuntime, | ||||
|   ); | ||||
|  | ||||
|   let activeNode: Node | undefined; | ||||
|   let scene: Group; | ||||
|   let updateViewerResult: (result: Int32Array) => void; | ||||
|   let activeNode = $state<Node | undefined>(undefined); | ||||
|   let scene = $state<Group>(null!); | ||||
|  | ||||
|   let graph = localStorage.getItem("graph") | ||||
|     ? JSON.parse(localStorage.getItem("graph")!) | ||||
|     : templates.defaultPlant; | ||||
|  | ||||
|   let graphInterface: ReturnType<typeof GraphInterface>; | ||||
|   $: manager = graphInterface?.manager; | ||||
|   $: managerStatus = manager?.status; | ||||
|   $: keymap = graphInterface?.keymap; | ||||
|   let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!); | ||||
|   let viewerComponent = $state<ReturnType<typeof Viewer>>(); | ||||
|   const manager = $derived(graphInterface?.manager); | ||||
|   const managerStatus = $derived(manager?.status); | ||||
|  | ||||
|   async function randomGenerate() { | ||||
|     if (!manager) return; | ||||
|     const g = manager.serialize(); | ||||
|     const s = { ...$graphSettings, randomSeed: true }; | ||||
|     const s = { ...graphSettings, randomSeed: true }; | ||||
|     await handleUpdate(g, s); | ||||
|   } | ||||
|  | ||||
| @@ -66,18 +70,18 @@ | ||||
|       callback: randomGenerate, | ||||
|     }, | ||||
|   ]); | ||||
|   let graphSettings = writable<Record<string, any>>({}); | ||||
|   let graphSettingTypes = {}; | ||||
|   let graphSettings = $state<Record<string, any>>({}); | ||||
|   let graphSettingTypes = $state({}); | ||||
|  | ||||
|   const handleUpdate = debounceAsyncFunction( | ||||
|     async (g: Graph, s: Record<string, any>) => { | ||||
|     async (g: Graph, s: Record<string, any> = graphSettings) => { | ||||
|       performanceStore.startRun(); | ||||
|       try { | ||||
|         let a = performance.now(); | ||||
|         const graphResult = await runtime.execute(g, s); | ||||
|         const graphResult = await runtime.execute(g, $state.snapshot(s)); | ||||
|         let b = performance.now(); | ||||
|  | ||||
|         if ($AppSettings.useWorker) { | ||||
|         if (appSettings.debug.useWorker) { | ||||
|           let perfData = await runtime.getPerformanceData(); | ||||
|           let lastRun = perfData?.at(-1); | ||||
|           if (lastRun?.total) { | ||||
| @@ -91,7 +95,7 @@ | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         updateViewerResult(graphResult); | ||||
|         viewerComponent?.update(graphResult); | ||||
|       } catch (error) { | ||||
|         console.log("errors", error); | ||||
|       } finally { | ||||
| @@ -100,32 +104,35 @@ | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   $: if (AppSettings) { | ||||
|     //@ts-ignore | ||||
|     AppSettingTypes.debug.stressTest.loadGrid.callback = () => { | ||||
|       graph = templates.grid($AppSettings.amount, $AppSettings.amount); | ||||
|     }; | ||||
|     //@ts-ignore | ||||
|     AppSettingTypes.debug.stressTest.loadTree.callback = () => { | ||||
|       graph = templates.tree($AppSettings.amount); | ||||
|     }; | ||||
|     //@ts-ignore | ||||
|     AppSettingTypes.debug.stressTest.lottaFaces.callback = () => { | ||||
|       graph = templates.lottaFaces; | ||||
|     }; | ||||
|     //@ts-ignore | ||||
|     AppSettingTypes.debug.stressTest.lottaNodes.callback = () => { | ||||
|       graph = templates.lottaNodes; | ||||
|     }; | ||||
|     //@ts-ignore | ||||
|     AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => { | ||||
|       graph = templates.lottaNodesAndFaces; | ||||
|     }; | ||||
|   } | ||||
|   // $ if (AppSettings) { | ||||
|   //   //@ts-ignore | ||||
|   //   AppSettingTypes.debug.stressTest.loadGrid.callback = () => { | ||||
|   //     graph = templates.grid($AppSettings.amount, $AppSettings.amount); | ||||
|   //   }; | ||||
|   //   //@ts-ignore | ||||
|   //   AppSettingTypes.debug.stressTest.loadTree.callback = () => { | ||||
|   //     graph = templates.tree($AppSettings.amount); | ||||
|   //   }; | ||||
|   //   //@ts-ignore | ||||
|   //   AppSettingTypes.debug.stressTest.lottaFaces.callback = () => { | ||||
|   //     graph = templates.lottaFaces; | ||||
|   //   }; | ||||
|   //   //@ts-ignore | ||||
|   //   AppSettingTypes.debug.stressTest.lottaNodes.callback = () => { | ||||
|   //     graph = templates.lottaNodes; | ||||
|   //   }; | ||||
|   //   //@ts-ignore | ||||
|   //   AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => { | ||||
|   //     graph = templates.lottaNodesAndFaces; | ||||
|   //   }; | ||||
|   // } | ||||
|  | ||||
|   function handleSave(graph: Graph) { | ||||
|     localStorage.setItem("graph", JSON.stringify(graph)); | ||||
|   } | ||||
|   onMount(() => { | ||||
|     handleUpdate(graph); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} /> | ||||
| @@ -134,10 +141,10 @@ | ||||
|   <Grid.Row> | ||||
|     <Grid.Cell> | ||||
|       <Viewer | ||||
|         perf={performanceStore} | ||||
|         bind:scene | ||||
|         bind:update={updateViewerResult} | ||||
|         centerCamera={$AppSettings.centerCamera} | ||||
|         bind:this={viewerComponent} | ||||
|         perf={performanceStore} | ||||
|         centerCamera={appSettings.centerCamera} | ||||
|       /> | ||||
|     </Grid.Cell> | ||||
|     <Grid.Cell> | ||||
| @@ -146,21 +153,21 @@ | ||||
|           bind:this={graphInterface} | ||||
|           {graph} | ||||
|           registry={nodeRegistry} | ||||
|           showGrid={appSettings.nodeInterface.showNodeGrid} | ||||
|           snapToGrid={appSettings.nodeInterface.snapToGrid} | ||||
|           bind:activeNode | ||||
|           showGrid={$AppSettings.showNodeGrid} | ||||
|           snapToGrid={$AppSettings.snapToGrid} | ||||
|           bind:showHelp={$AppSettings.showHelp} | ||||
|           bind:showHelp={appSettings.nodeInterface.showHelp} | ||||
|           bind:settings={graphSettings} | ||||
|           bind:settingTypes={graphSettingTypes} | ||||
|           onresult={(result) => handleUpdate(result, $graphSettings)} | ||||
|           onresult={(result) => handleUpdate(result)} | ||||
|           onsave={(graph) => handleSave(graph)} | ||||
|         /> | ||||
|         <Settings> | ||||
|           <Panel id="general" title="General" icon="i-tabler-settings"> | ||||
|             <NestedSettings | ||||
|               id="general" | ||||
|               value={_appSettings} | ||||
|               type={_AppSettingTypes} | ||||
|               value={appSettings} | ||||
|               type={AppSettingTypes} | ||||
|             /> | ||||
|           </Panel> | ||||
|           <Panel | ||||
| @@ -168,10 +175,12 @@ | ||||
|             title="Keyboard Shortcuts" | ||||
|             icon="i-tabler-keyboard" | ||||
|           > | ||||
|             <Keymap title="Application" keymap={applicationKeymap} /> | ||||
|             {#if keymap} | ||||
|               <Keymap title="Node-Editor" {keymap} /> | ||||
|             {/if} | ||||
|             <Keymap | ||||
|               keymaps={[ | ||||
|                 { keymap: applicationKeymap, title: "Application" }, | ||||
|                 { keymap: graphInterface.keymap, title: "Node-Editor" }, | ||||
|               ]} | ||||
|             /> | ||||
|           </Panel> | ||||
|           <Panel id="exports" title="Exporter" icon="i-tabler-package-export"> | ||||
|             <ExportSettings {scene} /> | ||||
| @@ -188,7 +197,7 @@ | ||||
|             id="performance" | ||||
|             title="Performance" | ||||
|             classes="text-red-400" | ||||
|             hidden={!$AppSettings.showPerformancePanel} | ||||
|             hidden={!appSettings.debug.showPerformancePanel} | ||||
|             icon="i-tabler-brand-speedtest" | ||||
|           > | ||||
|             {#if $performanceStore} | ||||
| @@ -199,7 +208,7 @@ | ||||
|             id="benchmark" | ||||
|             title="Benchmark" | ||||
|             classes="text-red-400" | ||||
|             hidden={!$AppSettings.showBenchmarkPanel} | ||||
|             hidden={!appSettings.debug.showBenchmarkPanel} | ||||
|             icon="i-tabler-graph" | ||||
|           > | ||||
|             <BenchmarkPanel run={randomGenerate} /> | ||||
|   | ||||
| @@ -17,15 +17,15 @@ | ||||
|       "type": "float", | ||||
|       "hidden": true, | ||||
|       "min": 0, | ||||
|       "max": 1, | ||||
|       "value": 0.5, | ||||
|       "max": 1 | ||||
|     }, | ||||
|     "depth": { | ||||
|       "type": "integer", | ||||
|       "min": 1, | ||||
|       "max": 10, | ||||
|       "hidden": true, | ||||
|       "value": 1, | ||||
|       "hidden": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -34,6 +34,6 @@ | ||||
|   } | ||||
|  | ||||
|   .content { | ||||
|     padding-left: 12px; | ||||
|     /* padding-left: 12px; */ | ||||
|   } | ||||
| </style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user