feat: initial design of parameter ui

This commit is contained in:
max_richter 2024-03-06 18:31:06 +01:00
parent 154c9a8383
commit 71ebbfc348
21 changed files with 1667 additions and 337 deletions

2
frontend/.gitignore vendored
View File

@ -7,6 +7,8 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
.svelte-kit/
vite.config.ts.timestamp* vite.config.ts.timestamp*
node_modules node_modules

View File

@ -0,0 +1,8 @@
import { defineConfig } from 'histoire'
import { HstSvelte } from '@histoire/plugin-svelte'
export default defineConfig({
plugins: [
HstSvelte(),
],
})

View File

@ -7,8 +7,10 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json", "tauri:dev": "tauri dev",
"tauri": "tauri" "story:dev": "histoire dev",
"story:build": "histoire build",
"story:preview": "histoire preview"
}, },
"dependencies": { "dependencies": {
"@sveltejs/kit": "^2.5.0", "@sveltejs/kit": "^2.5.0",
@ -21,10 +23,12 @@
"three": "^0.159.0" "three": "^0.159.0"
}, },
"devDependencies": { "devDependencies": {
"@histoire/plugin-svelte": "^0.17.9",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tauri-apps/cli": "2.0.0-beta.3", "@tauri-apps/cli": "2.0.0-beta.3",
"@tsconfig/svelte": "^5.0.2", "@tsconfig/svelte": "^5.0.2",
"histoire": "^0.17.9",
"internal-ip": "^7.0.0", "internal-ip": "^7.0.0",
"svelte": "^4.2.8", "svelte": "^4.2.8",
"svelte-check": "^3.4.6", "svelte-check": "^3.4.6",

View File

@ -1,36 +0,0 @@
<script lang="ts">
import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte";
import type { Graph } from "$lib/types";
const graph: Graph = {
edges: [],
nodes: [],
};
for (let i = 0; i < 40; i++) {
const x = i % 20;
const y = Math.floor(i / 20);
graph.nodes.push({
id: `${i.toString()}`,
tmp: {
visible: false,
},
position: {
x: x * 7.5,
y: y * 5,
},
type: "test",
});
graph.edges.push({
from: i.toString(),
to: (i + 1).toString(),
});
}
</script>
<Canvas shadows={false}>
<Scene {graph} />
</Canvas>

View File

@ -0,0 +1,28 @@
<script lang="ts">
import type { Hst } from "@histoire/plugin-svelte";
export let Hst: Hst;
import Background from "./Background.svelte";
import { Canvas } from "@threlte/core";
import Camera from "./Camera.svelte";
let width = globalThis.innerWidth || 100;
let height = globalThis.innerHeight || 100;
let cameraPosition: [number, number, number] = [0, 1, 0];
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Hst.Story>
<Canvas shadows={false}>
<Camera bind:position={cameraPosition} />
<Background
cx={cameraPosition[0]}
cy={cameraPosition[1]}
cz={cameraPosition[2]}
{width}
{height}
/>
</Canvas>
</Hst.Story>

View File

@ -0,0 +1,64 @@
<script lang="ts">
import { T } from "@threlte/core";
import { onMount } from "svelte";
import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag";
export let minZoom = 4;
export let maxZoom = 150;
export let cx = 0;
export let cy = 0;
export let cz = 30;
export let width = globalThis?.innerWidth || 100;
export let height = globalThis?.innerHeight || 100;
let bw = 2;
let bh = 2;
$: if (width && height) {
bw = width / cz;
bh = height / cz;
}
</script>
<T.Group position.x={cx} position.z={cy} position.y={-1.0}>
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
<T.PlaneGeometry args={[1, 1]} />
<T.ShaderMaterial
transparent
vertexShader={BackgroundVert}
fragmentShader={BackgroundFrag}
uniforms={{
cx: {
value: 0,
},
cy: {
value: 0,
},
cz: {
value: 30,
},
minz: {
value: minZoom,
},
maxz: {
value: maxZoom,
},
height: {
value: 100,
},
width: {
value: 100,
},
}}
uniforms.cx.value={cx}
uniforms.cy.value={cy}
uniforms.cz.value={cz}
uniforms.width.value={width}
uniforms.height.value={height}
/>
</T.Mesh>
</T.Group>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { T } from "@threlte/core";
import { OrbitControls } from "@threlte/extras";
import { onMount } from "svelte";
import { type OrthographicCamera } from "three";
export let camera: OrthographicCamera | undefined = undefined;
export let maxZoom = 150;
export let minZoom = 4;
let controls: OrbitControls | undefined = undefined;
export const position: [number, number, number] = [0, 1, 0];
function updateProps() {
if (!camera) return;
position[0] = camera.position.x;
position[1] = camera.position.z;
position[2] = camera.zoom;
}
onMount(() => {
updateProps();
controls?.addEventListener("change", updateProps);
return () => {
controls?.removeEventListener("change", updateProps);
};
});
</script>
<T.OrthographicCamera
bind:ref={camera}
makeDefault
position.y={1}
zoom={30}
on:create={({ ref, cleanup }) => {
ref.onBeforeRender = () => {
console.log(ref.position);
};
cleanup(() => {});
}}
>
<OrbitControls
bind:ref={controls}
enableZoom={true}
target.y={0}
rotateSpeed={0}
minPolarAngle={0}
maxPolarAngle={0}
enablePan={true}
zoomToCursor
{maxZoom}
{minZoom}
/>
</T.OrthographicCamera>

View File

@ -9,8 +9,6 @@
let samples = 20; let samples = 20;
console.log("edge");
const curve = new CubicBezierCurve( const curve = new CubicBezierCurve(
new Vector2(from.position.x + 20, from.position.y), new Vector2(from.position.x + 20, from.position.y),
new Vector2(from.position.x + 2, from.position.y), new Vector2(from.position.x + 2, from.position.y),
@ -33,10 +31,10 @@
last_from_x = new_x; last_from_x = new_x;
last_from_y = new_y; last_from_y = new_y;
} }
curve.v0.set(from.position.x + 5, from.position.y + 1.25); curve.v0.set(from.position.x + 5, from.position.y + 5 / 8);
curve.v1.set(from.position.x + 6, from.position.y + 1.25); curve.v1.set(from.position.x + 7, from.position.y + 5 / 8);
curve.v2.set(to.position.x - 1, to.position.y + 1.25); curve.v2.set(to.position.x - 2, to.position.y + 2.55);
curve.v3.set(to.position.x, to.position.y + 1.25); curve.v3.set(to.position.x + 0.2, to.position.y + 2.55);
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y)); points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
} }

View File

@ -0,0 +1,110 @@
<script lang="ts">
import Edge from "./Edge.svelte";
import { HTML } from "@threlte/extras";
import Node from "./Node.svelte";
import type { GraphManager } from "$lib/graph-manager";
import { onMount } from "svelte";
import { snapToGrid } from "$lib/helpers";
export let graph: GraphManager;
let edges = graph?.getEdges() || [];
export let cameraPosition: [number, number, number] = [0, 1, 0];
export let width = globalThis?.innerWidth || 100;
export let height = globalThis?.innerHeight || 100;
let mouseX = 0;
let mouseY = 0;
let mouseDown = false;
let mouseDownX = 0;
let mouseDownY = 0;
let activeNodeId: string;
function handleMouseMove(event: MouseEvent) {
if (!mouseDown) return;
mouseX =
cameraPosition[0] + (event.clientX - width / 2) / cameraPosition[2];
mouseY =
cameraPosition[1] + (event.clientY - height / 2) / cameraPosition[2];
if (!activeNodeId) return;
const node = graph.getNode(activeNodeId);
if (!node) return;
let newX =
(node?.tmp?.downX || 0) +
(event.clientX - mouseDownX) / cameraPosition[2];
let newY =
(node?.tmp?.downY || 0) +
(event.clientY - mouseDownY) / cameraPosition[2];
if (event.ctrlKey) {
newX = snapToGrid(newX, 2.5);
newY = snapToGrid(newY, 2.5);
}
node.position.x = newX;
node.position.y = newY;
node.position = node.position;
edges = [...edges];
graph.nodes = [...graph.nodes];
}
function handleMouseDown(ev: MouseEvent) {
activeNodeId = (ev?.target as HTMLElement)?.getAttribute("data-node-id")!;
mouseDown = true;
mouseDownX = ev.clientX;
mouseDownY = ev.clientY;
const node = graph.nodes.find((node) => node.id === activeNodeId);
if (!node) return;
node.tmp = node.tmp || {};
node.tmp.downX = node.position.x;
node.tmp.downY = node.position.y;
}
function handleMouseUp() {
mouseDown = false;
}
</script>
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} />
{#each edges as edge}
<Edge from={edge[0]} to={edge[1]} />
{/each}
<HTML transform={false}>
<div
role="tree"
tabindex="0"
class="wrapper"
style={`--cz: ${cameraPosition[2]}`}
on:mousedown={handleMouseDown}
>
{#each graph.nodes as node}
<Node {node} {graph} />
{/each}
</div>
</HTML>
<style>
:global(body) {
overflow: hidden;
}
.wrapper {
position: absolute;
z-index: 100;
width: 0px;
height: 0px;
transform: scale(calc(var(--cz) * 0.1));
}
</style>

View File

@ -0,0 +1,46 @@
<script lang="ts">
import type { GraphManager } from "$lib/graph-manager";
import type { Node } from "$lib/types";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
export let node: Node;
export let graph: GraphManager;
const type = graph.getNodeType(node.type);
const parameters = Object.entries(type?.inputs || {});
</script>
<div
class="node"
data-node-id={node.id}
style={`--nx:${node.position.x * 10}px;
--ny: ${node.position.y * 10}px`}
>
<NodeHeader {node} />
{#each parameters as [key, value], i}
<NodeParameter
value={node?.props?.[key]}
input={value}
label={key}
isLast={i == parameters.length - 1}
/>
{/each}
</div>
<style>
.node {
position: absolute;
box-sizing: border-box;
user-select: none !important;
cursor: pointer;
width: 50px;
color: white;
transform: translate(var(--nx), var(--ny));
z-index: 1;
font-weight: 300;
font-size: 0.5em;
}
</style>

View File

@ -0,0 +1,97 @@
<script lang="ts">
import type { Node } from "$lib/types";
export let node: Node;
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = 10;
let right_bump = true;
return `M0,100
${
corner
? ` V${corner}
Q0,0 ${corner / 4},0
H${100 - corner / 4}
Q100,0 100,${corner}
`
: ` V0
H100
`
}
V${y - height / 2}
${
right_bump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
V100
Z`.replace(/\s+/g, " ");
}
</script>
<div class="wrapper" data-node-id={node.id}>
<div class="content">
{node.type}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
width="100"
height="100"
preserveAspectRatio="none"
style={`
--path: path("${createPath({ depth: 3, height: 15 })}");
--hover-path: path("${createPath({ depth: 8, height: 24 })}");
`}
>
<path
vector-effect="non-scaling-stroke"
fill="none"
stroke="white"
stroke-width="0.1"
></path>
</svg>
</div>
<style>
.wrapper {
position: relative;
width: 100%;
height: 12.5px;
}
.wrapper > * {
pointer-events: none;
}
svg {
position: absolute;
top: 0;
left: 0;
z-index: -1;
box-sizing: border-box;
width: 100%;
height: calc(100% + 1px);
overflow: visible;
}
svg path {
stroke-width: 0.2px;
transition: 0.2s;
fill: #060606;
d: var(--path);
}
.content {
font-size: 0.5em;
display: flex;
align-items: center;
padding-left: 2px;
height: 100%;
}
svg:hover path {
d: var(--hover-path) !important;
}
</style>

View File

@ -0,0 +1,120 @@
<script lang="ts">
import type { NodeInput } from "$lib/types";
export let value: unknown;
export let input: NodeInput;
export let label: string;
export let isLast = false;
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = isLast ? 2 : 0;
let right_bump = false;
let left_bump = true;
return `M0,0
H100
V${y - height / 2}
${
right_bump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${
corner
? ` V${100 - corner}
Q100,100 ${100 - corner / 2},100
H${corner / 2}
Q0,100 0,${100 - corner}
`
: ` V100
H0
`
}
${
left_bump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}
</script>
<div class="wrapper">
<div class="content">
<label>{label}</label>
<input type="number" bind:value />
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
width="100"
height="100"
preserveAspectRatio="none"
style={`
--path: path("${createPath({ depth: 5, height: 15 })}");
--hover-path: path("${createPath({ depth: 8, height: 24 })}");
`}
>
<path
vector-effect="non-scaling-stroke"
fill="none"
stroke="white"
stroke-width="0.1"
></path>
</svg>
</div>
<style>
.wrapper {
position: relative;
width: 100%;
height: 25px;
}
.content {
position: relative;
padding: 2px 5px;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-around;
box-sizing: border-box;
}
input {
font-size: 0.5em;
width: 100%;
box-sizing: border-box;
}
label {
font-size: 0.5em;
}
svg {
position: absolute;
box-sizing: border-box;
pointer-events: none;
width: 100%;
height: 100%;
overflow: visible;
top: 0;
left: 0;
z-index: -1;
}
svg path {
stroke-width: 0.2px;
transition: 0.2s;
fill: #060606;
d: var(--path);
}
.wrapper:hover svg path {
d: var(--hover-path) !important;
}
</style>

View File

@ -1,24 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { Graph, Node } from "$lib/types";
import Edge from "./Edge.svelte";
import { T, useTask } from "@threlte/core";
import { settings } from "$lib/stores/settings";
import type { OrthographicCamera } from "three"; import type { OrthographicCamera } from "three";
import { HTML, OrbitControls } from "@threlte/extras"; import Camera from "./Camera.svelte";
import { onMount } from "svelte"; import Background from "./Background.svelte";
import BackgroundVert from "./Background.vert"; import type { GraphManager } from "$lib/graph-manager";
import BackgroundFrag from "./Background.frag"; import Graph from "./Graph.svelte";
import { max } from "three/examples/jsm/nodes/Nodes.js";
export let camera: OrthographicCamera; export let graph: GraphManager;
export let graph: Graph; const status = graph.status;
let cx = 0; let camera: OrthographicCamera;
let cy = 0; let cameraPosition: [number, number, number] = [0, 1, 0];
let cz = 30;
let bw = 2;
let bh = 2;
const minZoom = 4; const minZoom = 4;
const maxZoom = 150; const maxZoom = 150;
@ -26,230 +18,27 @@
let width = globalThis?.innerWidth || 100; let width = globalThis?.innerWidth || 100;
let height = globalThis?.innerHeight || 100; let height = globalThis?.innerHeight || 100;
let mouseX = 0; console.log({ graph });
let mouseY = 0;
let mouseDown = false;
let mouseDownX = 0;
let mouseDownY = 0;
let activeNodeId: string;
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "a") {
$settings.useHtml = !$settings.useHtml;
}
}
function snapToGrid(value: number) {
return Math.round(value / 2.5) * 2.5;
}
function handleMouseMove(event: MouseEvent) {
cx = camera.position.x || 0;
cy = camera.position.z || 0;
cz = camera.zoom || 30;
mouseX = cx + (event.clientX - width / 2) / cz;
mouseY = cy + (event.clientY - height / 2) / cz;
if (activeNodeId && mouseDown) {
graph.nodes = graph.nodes.map((node) => {
if (node.id === activeNodeId) {
node.position.x =
(node?.tmp?.downX || 0) + (event.clientX - mouseDownX) / cz;
node.position.y =
(node?.tmp?.downY || 0) + (event.clientY - mouseDownY) / cz;
if (event.ctrlKey) {
node.position.x = snapToGrid(node.position.x);
node.position.y = snapToGrid(node.position.y);
}
edges = [...edges];
}
return node;
});
}
}
let edges: [Node, Node][] = [];
function calculateEdges() {
edges = graph.edges
.map((edge) => {
const from = graph.nodes.find((node) => node.id === edge.from);
const to = graph.nodes.find((node) => node.id === edge.to);
if (!from || !to) return;
return [from, to] as const;
})
.filter(Boolean) as unknown as [Node, Node][];
}
calculateEdges();
function handleMouseDown(ev: MouseEvent) {
activeNodeId = ev?.target?.dataset?.nodeId;
mouseDown = true;
mouseDownX = ev.clientX;
mouseDownY = ev.clientY;
const node = graph.nodes.find((node) => node.id === activeNodeId);
if (!node) return;
node.tmp = node.tmp || {};
node.tmp.downX = node.position.x;
node.tmp.downY = node.position.y;
}
function handleMouseUp() {
mouseDown = false;
}
function updateCameraProps() {
cx = camera.position.x || 0;
cy = camera.position.z || 0;
cz = camera.zoom || 30;
width = window.innerWidth;
height = window.innerHeight;
bw = width / cz;
bh = height / cz;
}
onMount(() => {
updateCameraProps();
});
</script> </script>
<svelte:window <svelte:window bind:innerHeight={height} bind:innerWidth={width} />
on:keydown={handleKeyDown}
on:mousemove={handleMouseMove}
on:mouseup={handleMouseUp}
on:wheel={updateCameraProps}
on:resize={updateCameraProps}
/>
{#if true} <Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
<T.Group position.x={mouseX} position.z={mouseY}>
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2}>
<T.CircleGeometry args={[0.5, 16]} />
<T.MeshBasicMaterial color="green" />
</T.Mesh>
</T.Group>
{/if}
<T.Group position.x={cx} position.z={cy} position.y={-1.0}> <Background
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}> cx={cameraPosition[0]}
<T.PlaneGeometry args={[1, 1]} /> cy={cameraPosition[1]}
<T.ShaderMaterial cz={cameraPosition[2]}
transparent
vertexShader={BackgroundVert}
fragmentShader={BackgroundFrag}
uniforms={{
cx: {
value: 0,
},
cy: {
value: 0,
},
cz: {
value: 30,
},
minz: {
value: minZoom,
},
maxz: {
value: maxZoom,
},
height: {
value: 100,
},
width: {
value: 100,
},
}}
uniforms.cx.value={cx}
uniforms.cy.value={cy}
uniforms.cz.value={cz}
uniforms.width.value={width}
uniforms.height.value={height}
/>
</T.Mesh>
</T.Group>
<T.OrthographicCamera
bind:ref={camera}
makeDefault
position={[0, 1, 0]}
zoom={30}
>
<OrbitControls
enableZoom={true}
target.y={0}
rotateSpeed={0}
minPolarAngle={0}
maxPolarAngle={0}
enablePan={true}
zoomToCursor
{maxZoom} {maxZoom}
{minZoom} {minZoom}
{width}
{height}
/> />
</T.OrthographicCamera>
{#each edges as edge} {#if $status === "idle"}
<Edge from={edge[0]} to={edge[1]} /> <Graph {graph} {cameraPosition} />
{/each} {:else if $status === "loading"}
<a href="/graph">Loading...</a>
<HTML transform={false}> {:else if $status === "error"}
<div class="wrapper" style={`--cz: ${cz}`} on:mousedown={handleMouseDown}> <a href="/graph">Error</a>
{#each graph.nodes as node} {/if}
<div
class="node"
data-node-id={node.id}
style={`--nx:${node.position.x * 10}px;
--ny: ${node.position.y * 10}px`}
>
{node.id}
</div>
{/each}
</div>
</HTML>
<style>
:global(body) {
overflow: hidden;
}
.wrapper {
position: absolute;
z-index: 100;
width: 0px;
height: 0px;
transform: scale(calc(var(--cz) * 0.1));
}
.node {
position: absolute;
border-radius: 1px;
user-select: none !important;
cursor: pointer;
width: 50px;
height: 25px;
background: linear-gradient(-11.1grad, #000 0%, #0b0b0b 100%);
color: white;
transform: translate(var(--nx), var(--ny));
z-index: 1;
box-shadow: 0px 0px 0px calc(15px / var(--cz)) rgba(255, 255, 255, 0.3);
font-weight: 300;
font-size: 0.5em;
}
.node::after {
/* content: ""; */
position: absolute;
width: 100%;
height: 100%;
background: white;
border-radius: 2px;
transform: scale(1.01);
top: 0;
left: 0;
z-index: -1;
}
</style>

View File

@ -1,11 +0,0 @@
<script context="module" lang="ts">
export type Props = {
value: number;
};
</script>
<script lang="ts">
export let props = { value: 0 };
</script>
<p>output</p>

View File

@ -1,11 +0,0 @@
<script context="module" lang="ts">
export type Props = {
value: number;
};
</script>
<script lang="ts">
export let props = { value: 0 };
</script>
<input type="number" bind:value={props.value} />

View File

@ -1,6 +0,0 @@
import Test from './Test.svelte';
import Output from './Output.svelte';
export const nodes = {
test: Test,
output: Output,
} as const;

View File

@ -0,0 +1,114 @@
import { writable, type Writable } from "svelte/store";
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node } from "./types";
import { snapToGrid } from "./helpers";
const nodeTypes: NodeType[] = [
{
id: "input/float",
inputs: {
"value": { type: "float" },
},
outputs: ["float"],
},
{
id: "math",
inputs: {
"a": { type: "float" },
"b": { type: "float" },
},
outputs: ["float"],
},
]
export class NodeRegistry implements INodeRegistry {
getNode(id: string): NodeType | undefined {
return nodeTypes.find((nodeType) => nodeType.id === id);
}
}
export class GraphManager {
status: Writable<"loading" | "idle" | "error"> = writable("loading");
nodes: Node[] = [];
edges: { from: string, to: string }[] = [];
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) {
}
async load() {
const nodes = this.graph.nodes;
for (const node of nodes) {
const nodeType = this.getNodeType(node.type);
if (!nodeType) {
console.error(`Node type not found: ${node.type}`);
this.status.set("error");
return;
}
}
this.nodes = this.graph.nodes;
this.edges = this.graph.edges;
this.status.set("idle");
}
getNode(id: string) {
return this.nodes.find((node) => node.id === id);
}
public getNodeType(id: string): NodeType {
return this.nodeRegistry.getNode(id)!;
}
public getEdges() {
return this.edges
.map((edge) => {
const from = this.nodes.find((node) => node.id === edge.from);
const to = this.nodes.find((node) => node.id === edge.to);
if (!from || !to) return;
return [from, to] as const;
})
.filter(Boolean) as unknown as [Node, Node][];
}
static createEmptyGraph(): GraphManager {
const graph: Graph = {
edges: [],
nodes: [],
};
for (let i = 0; i < 40; i++) {
const x = i % 20;
const y = Math.floor(i / 20);
graph.nodes.push({
id: `${i.toString()}`,
tmp: {
visible: false,
},
position: {
x: x * 7.5,
y: y * 5,
},
props: i == 0 ? { value: 0 } : {},
type: i == 0 ? "input/float" : "math",
});
graph.edges.push({
from: i.toString(),
to: (i + 1).toString(),
});
}
return new GraphManager(graph);
}
}

View File

@ -0,0 +1,3 @@
export function snapToGrid(value: number, gridSize: number = 10) {
return Math.round(value / gridSize) * gridSize;
}

View File

@ -1,8 +1,7 @@
import { nodes } from "$lib/components/nodes"
export type Node = { export type Node = {
id: string; id: string;
type: keyof typeof nodes; type: string;
props?: Record<string, any>, props?: Record<string, any>,
tmp?: { tmp?: {
downX?: number; downX?: number;
@ -19,6 +18,42 @@ export type Node = {
} }
} }
type NodeInputFloat = {
type: "float";
value?: number;
min?: number;
max?: number;
}
type NodeInputInteger = {
type: "integer";
value?: number;
min?: number;
max?: number;
}
type NodeInputSelect = {
type: "select";
value?: string;
options: string[];
}
export type NodeInput = NodeInputFloat | NodeInputInteger | NodeInputSelect;
export type NodeType = {
id: string;
inputs?: Record<string, NodeInput>;
outputs?: string[];
meta?: {
title?: string;
}
}
export interface NodeRegistry {
getNode: (id: string) => NodeType | undefined;
}
export type Edge = { export type Edge = {
from: string; from: string;
to: string; to: string;

View File

@ -1,8 +1,14 @@
<script lang="ts"> <script lang="ts">
import App from "$lib/components/App.svelte";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Canvas } from "@threlte/core";
import Scene from "$lib/components/Scene.svelte";
import { GraphManager } from "$lib/graph-manager";
const graph = GraphManager.createEmptyGraph();
graph.load();
onMount(async () => { onMount(async () => {
try { try {
const res = await invoke("greet", { name: "Dude" }); const res = await invoke("greet", { name: "Dude" });
@ -21,7 +27,9 @@
</script> </script>
<div> <div>
<App /> <Canvas shadows={false}>
<Scene {graph} />
</Canvas>
</div> </div>
<style> <style>

File diff suppressed because it is too large Load Diff