feat: initial design of parameter ui
This commit is contained in:
parent
154c9a8383
commit
71ebbfc348
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -7,6 +7,8 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
.svelte-kit/
|
||||
|
||||
vite.config.ts.timestamp*
|
||||
|
||||
node_modules
|
||||
|
8
frontend/histoire.config.ts
Normal file
8
frontend/histoire.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'histoire'
|
||||
import { HstSvelte } from '@histoire/plugin-svelte'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
HstSvelte(),
|
||||
],
|
||||
})
|
@ -7,8 +7,10 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"tauri": "tauri"
|
||||
"tauri:dev": "tauri dev",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/kit": "^2.5.0",
|
||||
@ -21,10 +23,12 @@
|
||||
"three": "^0.159.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@histoire/plugin-svelte": "^0.17.9",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
"@tauri-apps/cli": "2.0.0-beta.3",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"histoire": "^0.17.9",
|
||||
"internal-ip": "^7.0.0",
|
||||
"svelte": "^4.2.8",
|
||||
"svelte-check": "^3.4.6",
|
||||
|
@ -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>
|
28
frontend/src/lib/components/Background.story.svelte
Normal file
28
frontend/src/lib/components/Background.story.svelte
Normal 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>
|
||||
|
64
frontend/src/lib/components/Background.svelte
Normal file
64
frontend/src/lib/components/Background.svelte
Normal 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>
|
56
frontend/src/lib/components/Camera.svelte
Normal file
56
frontend/src/lib/components/Camera.svelte
Normal 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>
|
@ -9,8 +9,6 @@
|
||||
|
||||
let samples = 20;
|
||||
|
||||
console.log("edge");
|
||||
|
||||
const curve = new CubicBezierCurve(
|
||||
new Vector2(from.position.x + 20, from.position.y),
|
||||
new Vector2(from.position.x + 2, from.position.y),
|
||||
@ -33,10 +31,10 @@
|
||||
last_from_x = new_x;
|
||||
last_from_y = new_y;
|
||||
}
|
||||
curve.v0.set(from.position.x + 5, from.position.y + 1.25);
|
||||
curve.v1.set(from.position.x + 6, from.position.y + 1.25);
|
||||
curve.v2.set(to.position.x - 1, to.position.y + 1.25);
|
||||
curve.v3.set(to.position.x, to.position.y + 1.25);
|
||||
curve.v0.set(from.position.x + 5, from.position.y + 5 / 8);
|
||||
curve.v1.set(from.position.x + 7, from.position.y + 5 / 8);
|
||||
curve.v2.set(to.position.x - 2, to.position.y + 2.55);
|
||||
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));
|
||||
}
|
||||
|
||||
|
110
frontend/src/lib/components/Graph.svelte
Normal file
110
frontend/src/lib/components/Graph.svelte
Normal 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>
|
46
frontend/src/lib/components/Node.svelte
Normal file
46
frontend/src/lib/components/Node.svelte
Normal 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>
|
97
frontend/src/lib/components/NodeHeader.svelte
Normal file
97
frontend/src/lib/components/NodeHeader.svelte
Normal 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>
|
120
frontend/src/lib/components/NodeParameter.svelte
Normal file
120
frontend/src/lib/components/NodeParameter.svelte
Normal 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>
|
@ -1,24 +1,16 @@
|
||||
<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 { HTML, OrbitControls } from "@threlte/extras";
|
||||
import { onMount } from "svelte";
|
||||
import BackgroundVert from "./Background.vert";
|
||||
import BackgroundFrag from "./Background.frag";
|
||||
import { max } from "three/examples/jsm/nodes/Nodes.js";
|
||||
import Camera from "./Camera.svelte";
|
||||
import Background from "./Background.svelte";
|
||||
import type { GraphManager } from "$lib/graph-manager";
|
||||
import Graph from "./Graph.svelte";
|
||||
|
||||
export let camera: OrthographicCamera;
|
||||
export let graph: GraphManager;
|
||||
|
||||
export let graph: Graph;
|
||||
const status = graph.status;
|
||||
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
let cz = 30;
|
||||
let bw = 2;
|
||||
let bh = 2;
|
||||
let camera: OrthographicCamera;
|
||||
let cameraPosition: [number, number, number] = [0, 1, 0];
|
||||
|
||||
const minZoom = 4;
|
||||
const maxZoom = 150;
|
||||
@ -26,230 +18,27 @@
|
||||
let width = globalThis?.innerWidth || 100;
|
||||
let height = globalThis?.innerHeight || 100;
|
||||
|
||||
let mouseX = 0;
|
||||
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();
|
||||
});
|
||||
console.log({ graph });
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={handleKeyDown}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:mouseup={handleMouseUp}
|
||||
on:wheel={updateCameraProps}
|
||||
on:resize={updateCameraProps}
|
||||
/>
|
||||
<svelte:window bind:innerHeight={height} bind:innerWidth={width} />
|
||||
|
||||
{#if true}
|
||||
<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}
|
||||
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
|
||||
|
||||
<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>
|
||||
|
||||
<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
|
||||
<Background
|
||||
cx={cameraPosition[0]}
|
||||
cy={cameraPosition[1]}
|
||||
cz={cameraPosition[2]}
|
||||
{maxZoom}
|
||||
{minZoom}
|
||||
/>
|
||||
</T.OrthographicCamera>
|
||||
{width}
|
||||
{height}
|
||||
/>
|
||||
|
||||
{#each edges as edge}
|
||||
<Edge from={edge[0]} to={edge[1]} />
|
||||
{/each}
|
||||
|
||||
<HTML transform={false}>
|
||||
<div class="wrapper" style={`--cz: ${cz}`} on:mousedown={handleMouseDown}>
|
||||
{#each graph.nodes as node}
|
||||
<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>
|
||||
{#if $status === "idle"}
|
||||
<Graph {graph} {cameraPosition} />
|
||||
{:else if $status === "loading"}
|
||||
<a href="/graph">Loading...</a>
|
||||
{:else if $status === "error"}
|
||||
<a href="/graph">Error</a>
|
||||
{/if}
|
||||
|
@ -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>
|
@ -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} />
|
@ -1,6 +0,0 @@
|
||||
import Test from './Test.svelte';
|
||||
import Output from './Output.svelte';
|
||||
export const nodes = {
|
||||
test: Test,
|
||||
output: Output,
|
||||
} as const;
|
114
frontend/src/lib/graph-manager.ts
Normal file
114
frontend/src/lib/graph-manager.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
3
frontend/src/lib/helpers.ts
Normal file
3
frontend/src/lib/helpers.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function snapToGrid(value: number, gridSize: number = 10) {
|
||||
return Math.round(value / gridSize) * gridSize;
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import { nodes } from "$lib/components/nodes"
|
||||
|
||||
export type Node = {
|
||||
id: string;
|
||||
type: keyof typeof nodes;
|
||||
type: string;
|
||||
props?: Record<string, any>,
|
||||
tmp?: {
|
||||
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 = {
|
||||
from: string;
|
||||
to: string;
|
||||
|
@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import App from "$lib/components/App.svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
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 () => {
|
||||
try {
|
||||
const res = await invoke("greet", { name: "Dude" });
|
||||
@ -21,7 +27,9 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<App />
|
||||
<Canvas shadows={false}>
|
||||
<Scene {graph} />
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
958
pnpm-lock.yaml
958
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user