feat/drop-node-on-connection #18

Merged
max merged 4 commits from feat/drop-node-on-connection into main 2026-01-20 17:47:04 +01:00
8 changed files with 99 additions and 96 deletions
Showing only changes of commit 63997ec262 - Show all commits

View File

@@ -40,7 +40,7 @@
}, },
"excludes": [ "excludes": [
"**/node_modules", "**/node_modules",
"**/*-lock.json" "**/*-lock.yaml"
], ],
"plugins": [ "plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm", "https://plugins.dprint.dev/typescript-0.95.13.wasm",

View File

@@ -1,16 +1,15 @@
import { animate, lerp } from "$lib/helpers"; import { animate, lerp } from '$lib/helpers';
import type { createKeyMap } from "$lib/helpers/createKeyMap"; import type { createKeyMap } from '$lib/helpers/createKeyMap';
import FileSaver from "file-saver"; import { panelState } from '$lib/sidebar/PanelState.svelte';
import type { GraphManager } from "./graph-manager.svelte"; import FileSaver from 'file-saver';
import type { GraphState } from "./graph-state.svelte"; import type { GraphManager } from './graph-manager.svelte';
import type { GraphState } from './graph-state.svelte';
type Keymap = ReturnType<typeof createKeyMap>; type Keymap = ReturnType<typeof createKeyMap>;
export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) { export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) {
keymap.addShortcut({ keymap.addShortcut({
key: "l", key: 'l',
description: "Select linked nodes", description: 'Select linked nodes',
callback: () => { callback: () => {
const activeNode = graph.getNode(graphState.activeNodeId); const activeNode = graph.getNode(graphState.activeNodeId);
if (activeNode) { if (activeNode) {
@@ -20,56 +19,54 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
graphState.selectedNodes.add(node.id); graphState.selectedNodes.add(node.id);
} }
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "?", key: '?',
description: "Toggle Help", description: 'Toggle Help',
callback: () => { callback: () => {
// TODO: fix this panelState.setActivePanel('shortcuts');
// showHelp = !showHelp; }
},
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "c", key: 'c',
ctrl: true, ctrl: true,
description: "Copy active nodes", description: 'Copy active nodes',
callback: () => graphState.copyNodes(), callback: () => graphState.copyNodes()
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "v", key: 'v',
ctrl: true, ctrl: true,
description: "Paste nodes", description: 'Paste nodes',
callback: () => graphState.pasteNodes(), callback: () => graphState.pasteNodes()
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "Escape", key: 'Escape',
description: "Deselect nodes", description: 'Deselect nodes',
callback: () => { callback: () => {
graphState.activeNodeId = -1; graphState.activeNodeId = -1;
graphState.clearSelection(); graphState.clearSelection();
graphState.edgeEndPosition = null; graphState.edgeEndPosition = null;
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "A", key: 'A',
shift: true, shift: true,
description: "Add new Node", description: 'Add new Node',
callback: () => { callback: () => {
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]]; graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: ".", key: '.',
description: "Center camera", description: 'Center camera',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
@@ -90,67 +87,67 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
animate(500, (a: number) => { animate(500, (a: number) => {
graphState.cameraPosition[0] = lerp(camX, average[0], ease(a)); graphState.cameraPosition[0] = lerp(camX, average[0], ease(a));
graphState.cameraPosition[1] = lerp(camY, average[1], ease(a)); graphState.cameraPosition[1] = lerp(camY, average[1], ease(a));
graphState.cameraPosition[2] = lerp(camZ, 2, ease(a)) graphState.cameraPosition[2] = lerp(camZ, 2, ease(a));
if (graphState.mouseDown) return false; if (graphState.mouseDown) return false;
}); });
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "a", key: 'a',
ctrl: true, ctrl: true,
preventDefault: true, preventDefault: true,
description: "Select all nodes", description: 'Select all nodes',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
for (const node of graph.nodes.keys()) { for (const node of graph.nodes.keys()) {
graphState.selectedNodes.add(node); graphState.selectedNodes.add(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "z", key: 'z',
ctrl: true, ctrl: true,
description: "Undo", description: 'Undo',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
graph.undo(); graph.undo();
for (const node of graph.nodes.values()) { for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "y", key: 'y',
ctrl: true, ctrl: true,
description: "Redo", description: 'Redo',
callback: () => { callback: () => {
graph.redo(); graph.redo();
for (const node of graph.nodes.values()) { for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "s", key: 's',
ctrl: true, ctrl: true,
description: "Save", description: 'Save',
preventDefault: true, preventDefault: true,
callback: () => { callback: () => {
const state = graph.serialize(); const state = graph.serialize();
const blob = new Blob([JSON.stringify(state)], { const blob = new Blob([JSON.stringify(state)], {
type: "application/json;charset=utf-8", type: 'application/json;charset=utf-8'
}); });
FileSaver.saveAs(blob, "nodarium-graph.json"); FileSaver.saveAs(blob, 'nodarium-graph.json');
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: ["Delete", "Backspace", "x"], key: ['Delete', 'Backspace', 'x'],
description: "Delete selected nodes", description: 'Delete selected nodes',
callback: (event) => { callback: (event) => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
graph.startUndoGroup(); graph.startUndoGroup();
@@ -171,20 +168,18 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
graphState.clearSelection(); graphState.clearSelection();
} }
graph.saveUndoGroup(); graph.saveUndoGroup();
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "f", key: 'f',
description: "Smart Connect Nodes", description: 'Smart Connect Nodes',
callback: () => { callback: () => {
const nodes = [...graphState.selectedNodes.values()] const nodes = [...graphState.selectedNodes.values()]
.map((g) => graph.getNode(g)) .map((g) => graph.getNode(g))
.filter((n) => !!n); .filter((n) => !!n);
const edge = graph.smartConnect(nodes[0], nodes[1]); const edge = graph.smartConnect(nodes[0], nodes[1]);
if (!edge) graph.smartConnect(nodes[1], nodes[0]); if (!edge) graph.smartConnect(nodes[1], nodes[0]);
}, }
}); });
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getContext, type Snippet } from "svelte"; import { type Snippet } from "svelte";
import type { PanelState } from "./PanelState.svelte"; import { panelState } from "./PanelState.svelte";
const { const {
id, id,
@@ -18,8 +18,6 @@
children?: Snippet; children?: Snippet;
}>(); }>();
const panelState = getContext<PanelState>("panel-state");
const panel = panelState.registerPanel(id, icon, classes, hidden); const panel = panelState.registerPanel(id, icon, classes, hidden);
$effect(() => { $effect(() => {
panel.hidden = hidden; panel.hidden = hidden;

View File

@@ -1,15 +1,14 @@
import { localState } from "$lib/helpers/localState.svelte"; import { localState } from '$lib/helpers/localState.svelte';
type Panel = { type Panel = {
icon: string; icon: string;
classes: string; classes: string;
hidden?: boolean; hidden?: boolean;
} };
export class PanelState {
class PanelState {
panels = $state<Record<string, Panel>>({}); panels = $state<Record<string, Panel>>({});
activePanel = localState<string | boolean>("node.activePanel", "") activePanel = localState<string | boolean>('node.activePanel', '');
get keys() { get keys() {
return Object.keys(this.panels); return Object.keys(this.panels);
@@ -19,7 +18,7 @@ export class PanelState {
const state = $state({ const state = $state({
icon: icon, icon: icon,
classes: classes, classes: classes,
hidden: hidden, hidden: hidden
}); });
this.panels[id] = state; this.panels[id] = state;
return state; return state;
@@ -29,7 +28,13 @@ export class PanelState {
if (this.activePanel.value) { if (this.activePanel.value) {
this.activePanel.value = false; this.activePanel.value = false;
} else { } else {
this.activePanel.value = this.keys[0] this.activePanel.value = this.keys[0];
} }
} }
public setActivePanel(panelId: string) {
this.activePanel.value = panelId;
}
} }
export const panelState = new PanelState();

View File

@@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import { setContext, type Snippet } from "svelte"; import { type Snippet } from "svelte";
import { PanelState } from "./PanelState.svelte"; import { panelState as state } from "./PanelState.svelte";
const state = new PanelState();
setContext("panel-state", state);
const { children } = $props<{ children?: Snippet }>(); const { children } = $props<{ children?: Snippet }>();
</script> </script>

View File

@@ -2,11 +2,9 @@
import type { NodeInput } from '@nodarium/types'; import type { NodeInput } from '@nodarium/types';
import Checkbox from './inputs/Checkbox.svelte'; import Checkbox from './inputs/Checkbox.svelte';
import Float from './inputs/Float.svelte'; import Number from './inputs/Number.svelte';
import Integer from './inputs/Integer.svelte';
import Select from './inputs/Select.svelte'; import Select from './inputs/Select.svelte';
import Vec3 from './inputs/Vec3.svelte'; import Vec3 from './inputs/Vec3.svelte';
// import Number from './inputs/Number.svelte';
interface Props { interface Props {
input: NodeInput; input: NodeInput;
@@ -18,9 +16,9 @@
</script> </script>
{#if input.type === 'float'} {#if input.type === 'float'}
<Float bind:value min={input?.min} max={input?.max} /> <Number bind:value min={input?.min} max={input?.max} step={0.01} />
{:else if input.type === 'integer'} {:else if input.type === 'integer'}
<Integer bind:value min={input?.min} max={input?.max} /> <Number bind:value min={input?.min} max={input?.max} />
{:else if input.type === 'boolean'} {:else if input.type === 'boolean'}
<Checkbox bind:value {id} /> <Checkbox bind:value {id} />
{:else if input.type === 'select'} {:else if input.type === 'select'}

View File

@@ -112,10 +112,7 @@
onmousedown={handleMouseDown} onmousedown={handleMouseDown}
onmouseup={handleMouseUp} onmouseup={handleMouseUp}
> >
{#if typeof min !== 'undefined' && typeof max !== 'undefined'} <div class="">
<span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
></span>
{/if}
<button onclick={() => handleChange(-step)}>-</button> <button onclick={() => handleChange(-step)}>-</button>
<input <input
bind:value bind:value
@@ -129,6 +126,11 @@
/> />
<button onclick={() => handleChange(+step)}>+</button> <button onclick={() => handleChange(+step)}>+</button>
</div>
{#if typeof min !== 'undefined' && typeof max !== 'undefined'}
<span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
></span>
{/if}
</div> </div>
<style> <style>

View File

@@ -12,8 +12,10 @@
step = 1, step = 1,
min = $bindable(0), min = $bindable(0),
max = $bindable(1), max = $bindable(1),
id id: _id
}: Props = $props(); }: Props = $props();
const uid = $props.id();
const id = $derived(_id || uid);
if (min > max) { if (min > max) {
[min, max] = [max, min]; [min, max] = [max, min];
@@ -26,7 +28,7 @@
return +parseFloat(input + '').toPrecision(2); return +parseFloat(input + '').toPrecision(2);
} }
let inputEl: HTMLInputElement | undefined = $state(); let inputEl = $state() as HTMLInputElement;
let prev = -1; let prev = -1;
function update() { function update() {
@@ -82,6 +84,10 @@
</div> </div>
<style> <style>
:root {
--slider-height: 4px;
}
.component-wrapper { .component-wrapper {
display: flex; display: flex;
background-color: var(--layer-2, #4b4b4b); background-color: var(--layer-2, #4b4b4b);
@@ -145,7 +151,7 @@
position: absolute; position: absolute;
appearance: none; appearance: none;
width: 100%; width: 100%;
height: 3px; height: var(--slider-height);
background: var(--layer-2, #4b4b4b); background: var(--layer-2, #4b4b4b);
cursor: pointer; cursor: pointer;
} }
@@ -154,16 +160,18 @@
input[type='range']::-webkit-slider-thumb { input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 0px; width: 12px;
height: 0px; height: var(--slider-height);
background: var(--text-color);
box-shadow: none; box-shadow: none;
} }
/* Thumb: for Firefox */ /* Thumb: for Firefox */
input[type='range']::-moz-range-thumb { input[type='range']::-moz-range-thumb {
border: none; border: none;
width: 0px; width: 12px;
height: 0px; height: var(--slider-height);
background: var(--text-color);
box-shadow: none; box-shadow: none;
} }
</style> </style>