feat: implement first dragging edge prototype

This commit is contained in:
max_richter 2024-03-11 02:02:04 +01:00
parent a74bd0cf16
commit 1d6ae65630
9 changed files with 127 additions and 34 deletions

View File

@ -59,6 +59,7 @@
<OrbitControls <OrbitControls
bind:ref={controls} bind:ref={controls}
enableZoom={true} enableZoom={true}
zoomSpeed={2}
target.y={0} target.y={0}
rotateSpeed={0} rotateSpeed={0}
minPolarAngle={0} minPolarAngle={0}

View File

@ -4,10 +4,10 @@
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras"; import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { CubicBezierCurve, Vector2, Vector3 } from "three"; import { CubicBezierCurve, Vector2, Vector3 } from "three";
export let from: Node; export let from: { position: { x: number; y: number } };
export let to: Node; export let to: { position: { x: number; y: number } };
let samples = 25; let samples = 12;
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),

View File

@ -4,29 +4,36 @@
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import Node from "./Node.svelte"; import Node from "./Node.svelte";
import type { GraphManager } from "$lib/graph-manager"; import type { GraphManager } from "$lib/graph-manager";
import { onMount } from "svelte";
import { snapToGrid } from "$lib/helpers"; import { snapToGrid } from "$lib/helpers";
import { writable, type Writable } from "svelte/store";
export let graph: GraphManager; 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 width = globalThis?.innerWidth || 100;
export let height = globalThis?.innerHeight || 100; export let height = globalThis?.innerHeight || 100;
let edges = graph?.getEdges() || [];
export let cameraPosition: [number, number, number] = [0, 1, 0];
let cameraBounds = [-Infinity, Infinity, -Infinity, Infinity];
$: if (cameraPosition[0]) {
cameraBounds[0] = cameraPosition[0] - width / cameraPosition[2];
cameraBounds[1] = cameraPosition[0] + width / cameraPosition[2];
cameraBounds[2] = cameraPosition[1] - height / cameraPosition[2];
cameraBounds[3] = cameraPosition[1] + height / cameraPosition[2];
}
let mouseX = 0; let mouseX = 0;
let mouseY = 0; let mouseY = 0;
let mouseDown = false; let mouseDown: Writable<false | { x: number; y: number; socket: any }> =
let mouseDownX = 0; writable(false);
let mouseDownY = 0;
let activeNodeId: string; let activeNodeId: string;
function handleMouseMove(event: MouseEvent) { function handleMouseMove(event: MouseEvent) {
if (!mouseDown) return; if (!$mouseDown) return;
mouseX = mouseX =
cameraPosition[0] + (event.clientX - width / 2) / cameraPosition[2]; cameraPosition[0] + (event.clientX - width / 2) / cameraPosition[2];
@ -39,12 +46,15 @@
if (!node) return; if (!node) return;
if (!node.tmp) node.tmp = {};
node.tmp.isMoving = true;
let newX = let newX =
(node?.tmp?.downX || 0) + (node?.tmp?.downX || 0) +
(event.clientX - mouseDownX) / cameraPosition[2]; (event.clientX - $mouseDown.x) / cameraPosition[2];
let newY = let newY =
(node?.tmp?.downY || 0) + (node?.tmp?.downY || 0) +
(event.clientY - mouseDownY) / cameraPosition[2]; (event.clientY - $mouseDown.y) / cameraPosition[2];
if (event.ctrlKey) { if (event.ctrlKey) {
const snapLevel = getSnapLevel(); const snapLevel = getSnapLevel();
@ -59,11 +69,15 @@
} }
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
activeNodeId = (ev?.target as HTMLElement)?.getAttribute("data-node-id")!; for (const node of ev.composedPath()) {
activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
"data-node-id",
)!;
if (activeNodeId) break;
}
if (!activeNodeId) return;
mouseDown = true; $mouseDown = { x: ev.clientX, y: ev.clientY, socket: null };
mouseDownX = ev.clientX;
mouseDownY = ev.clientY;
const node = graph.nodes.find((node) => node.id === activeNodeId); const node = graph.nodes.find((node) => node.id === activeNodeId);
if (!node) return; if (!node) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
@ -84,12 +98,23 @@
return 1; return 1;
} }
function isNodeInView(node: any) {
return (
node.position.x > cameraBounds[0] &&
node.position.x < cameraBounds[1] &&
node.position.y > cameraBounds[2] &&
node.position.y < cameraBounds[3]
);
}
function handleMouseUp() { function handleMouseUp() {
mouseDown = false; $mouseDown = false;
const node = graph.getNode(activeNodeId); const node = graph.getNode(activeNodeId);
if (!node) return; if (!node) return;
node.tmp = node.tmp || {};
node.tmp.isMoving = false;
const snapLevel = getSnapLevel(); const snapLevel = getSnapLevel();
node.position.x = snapToGrid(node.position.x, 5 / snapLevel); node.position.x = snapToGrid(node.position.x, 5 / snapLevel);
node.position.y = snapToGrid(node.position.y, 5 / snapLevel); node.position.y = snapToGrid(node.position.y, 5 / snapLevel);
@ -99,22 +124,38 @@
} }
</script> </script>
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} /> <svelte:document
on:mousemove={handleMouseMove}
on:mouseup={handleMouseUp}
on:mousedown={handleMouseDown}
/>
{#each edges as edge} {#each edges as edge}
<Edge from={edge[0]} to={edge[1]} /> <Edge from={edge[0]} to={edge[1]} />
{/each} {/each}
{#if $mouseDown && $mouseDown?.socket}
<Edge
from={{ position: $mouseDown.socket }}
to={{ position: { x: mouseX, y: mouseY } }}
/>
{/if}
<HTML transform={false}> <HTML transform={false}>
<div <div
role="tree" role="tree"
tabindex="0" tabindex="0"
class="wrapper" class="wrapper"
class:zoom-small={cameraPosition[2] < 10}
style={`--cz: ${cameraPosition[2]}`} style={`--cz: ${cameraPosition[2]}`}
on:mousedown={handleMouseDown}
> >
{#each graph.nodes as node} {#each graph.nodes as node}
<Node {node} {graph} /> <Node
{node}
{graph}
inView={cameraPosition && isNodeInView(node)}
{mouseDown}
/>
{/each} {/each}
</div> </div>
</HTML> </HTML>

View File

@ -1,12 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { GraphManager } from "$lib/graph-manager"; import type { GraphManager } from "$lib/graph-manager";
import type { Node } from "$lib/types"; import type { Node } from "$lib/types";
import type { Writable } from "svelte/store";
import NodeHeader from "./NodeHeader.svelte"; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte"; import NodeParameter from "./NodeParameter.svelte";
export let node: Node; export let node: Node;
export let graph: GraphManager; export let graph: GraphManager;
export let inView = true;
export let mouseDown: Writable<false | { x: number; y: number; socket: any }>;
const type = graph.getNodeType(node.type); const type = graph.getNodeType(node.type);
const parameters = Object.entries(type?.inputs || {}); const parameters = Object.entries(type?.inputs || {});
@ -14,11 +19,13 @@
<div <div
class="node" class="node"
class:in-view={inView}
class:is-moving={node?.tmp?.isMoving}
data-node-id={node.id} data-node-id={node.id}
style={`--nx:${node.position.x * 10}px; style={`--nx:${node.position.x * 10}px;
--ny: ${node.position.y * 10}px`} --ny: ${node.position.y * 10}px`}
> >
<NodeHeader {node} /> <NodeHeader {node} {mouseDown} />
{#each parameters as [key, value], i} {#each parameters as [key, value], i}
<NodeParameter <NodeParameter
@ -38,9 +45,20 @@
cursor: pointer; cursor: pointer;
width: 50px; width: 50px;
color: white; color: white;
transform: translate(var(--nx), var(--ny)); transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1; z-index: 1;
font-weight: 300; font-weight: 300;
font-size: 0.5em; font-size: 0.5em;
display: none;
transition: transform 0.2s ease;
}
.node.is-moving {
z-index: 100;
transition: none !important;
}
.node.in-view {
display: unset;
} }
</style> </style>

View File

@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "$lib/types"; import type { Node } from "$lib/types";
import type { Writable } from "svelte/store";
export let node: Node; export let node: Node;
export let mouseDown: Writable<false | { x: number; y: number; socket: any }>;
function createPath({ depth = 8, height = 20, y = 50 } = {}) { function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = 10; let corner = 10;
@ -29,6 +32,17 @@
V100 V100
Z`.replace(/\s+/g, " "); Z`.replace(/\s+/g, " ");
} }
function handleMouseDown(event: MouseEvent) {
$mouseDown = {
x: event.clientX,
y: event.clientY,
socket: { x: node.position.x + 5, y: node.position.y + 0.65 },
};
console.log("click");
event.stopPropagation();
event.preventDefault();
}
</script> </script>
<div class="wrapper" data-node-id={node.id}> <div class="wrapper" data-node-id={node.id}>
@ -43,9 +57,17 @@
preserveAspectRatio="none" preserveAspectRatio="none"
style={` style={`
--path: path("${createPath({ depth: 5, height: 27, y: 48.2 })}"); --path: path("${createPath({ depth: 5, height: 27, y: 48.2 })}");
--hover-path: path("${createPath({ depth: 8, height: 24 })}"); --hover-path: path("${createPath({ depth: 6, height: 33, y: 48.2 })}");
`} `}
> >
<ellipse
cx="100"
cy="48"
rx="2.7"
ry="10"
fill="red"
on:mousedown={handleMouseDown}
/>
<path <path
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
fill="none" fill="none"
@ -62,7 +84,7 @@
height: 12.5px; height: 12.5px;
} }
.wrapper > * { .wrapper > * {
pointer-events: none; /* pointer-events: none; */
} }
svg { svg {

View File

@ -56,7 +56,7 @@
preserveAspectRatio="none" preserveAspectRatio="none"
style={` style={`
--path: path("${createPath({ depth: 5, height: 15, y: 48.2 })}"); --path: path("${createPath({ depth: 5, height: 15, y: 48.2 })}");
--hover-path: path("${createPath({ depth: 8, height: 24 })}"); --hover-path: path("${createPath({ depth: 8, height: 24, y: 48.2 })}");
`} `}
> >
<path vector-effect="non-scaling-stroke"></path> <path vector-effect="non-scaling-stroke"></path>
@ -78,6 +78,12 @@
height: 100%; height: 100%;
justify-content: space-around; justify-content: space-around;
box-sizing: border-box; box-sizing: border-box;
opacity: calc((2 * var(--cz)) / 150 - 0.5);
opacity: calc((var(--cz) - 10) / 20);
}
:global(.zoom-small) .content {
/* display: none; */
} }
input { input {
@ -102,7 +108,7 @@
svg { svg {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
pointer-events: none; /* pointer-events: none; */
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: visible; overflow: visible;

View File

@ -75,16 +75,18 @@ export class GraphManager {
} }
static createEmptyGraph(): GraphManager { static createEmptyGraph({ width = 20, height = 20 } = {}): GraphManager {
const graph: Graph = { const graph: Graph = {
edges: [], edges: [],
nodes: [], nodes: [],
}; };
for (let i = 0; i < 40; i++) { const amount = width * height;
const x = i % 20;
const y = Math.floor(i / 20); for (let i = 0; i < amount; i++) {
const x = i % width;
const y = Math.floor(i / height);
graph.nodes.push({ graph.nodes.push({
id: `${i.toString()}`, id: `${i.toString()}`,
@ -93,7 +95,7 @@ export class GraphManager {
}, },
position: { position: {
x: x * 7.5, x: x * 7.5,
y: y * 5, y: y * 10,
}, },
props: i == 0 ? { value: 0 } : {}, props: i == 0 ? { value: 0 } : {},
type: i == 0 ? "input/float" : "math", type: i == 0 ? "input/float" : "math",

View File

@ -7,6 +7,7 @@ export type Node = {
downX?: number; downX?: number;
downY?: number; downY?: number;
visible?: boolean; visible?: boolean;
isMoving?: boolean;
}, },
meta?: { meta?: {
title?: string; title?: string;

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { PerfMonitor } from "@threlte/extras";
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import Scene from "$lib/components/Scene.svelte"; import Scene from "$lib/components/Scene.svelte";
@ -27,7 +28,8 @@
</script> </script>
<div> <div>
<Canvas shadows={false}> <Canvas shadows={false} renderMode="on-demand">
<PerfMonitor />
<Scene {graph} /> <Scene {graph} />
</Canvas> </Canvas>
</div> </div>