feat: add shortcut viewer

This commit is contained in:
max_richter 2024-04-19 21:51:07 +02:00
parent f1fcf78f6c
commit e575974872
9 changed files with 232 additions and 67 deletions

View File

@ -20,7 +20,7 @@
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { MeshLineMaterial } from "@threlte/extras"; import { MeshLineMaterial } from "@threlte/extras";
import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three"; import { BufferGeometry, Color, MeshBasicMaterial, Vector3 } from "three";
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry.js"; import { createEdgeGeometry } from "./createEdgeGeometry.js";
@ -74,6 +74,13 @@
$: if (from || to) { $: if (from || to) {
update(); update();
} }
const lineColor = new Color($colors.outline);
$: if ($colors.outline) {
lineColor.copyLinearToSRGB($colors.outline);
console.log("lineColor", lineColor);
}
</script> </script>
<T.Mesh <T.Mesh
@ -83,7 +90,7 @@
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
material={circleMaterial} material={circleMaterial}
> >
<T.CircleGeometry args={[0.3, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
<T.Mesh <T.Mesh
@ -93,11 +100,16 @@
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
material={circleMaterial} material={circleMaterial}
> >
<T.CircleGeometry args={[0.3, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
{#if geometry} {#if geometry}
<T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}> <T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}>
<MeshLineMaterial width={3} attenuate={false} color={$colors.outline} /> <MeshLineMaterial
width={3}
attenuate={false}
color={lineColor}
toneMapped={false}
/>
</T.Mesh> </T.Mesh>
{/if} {/if}

View File

@ -1,16 +1,10 @@
<script lang="ts"> <script lang="ts">
import { animate, lerp, snapToGrid } from "../helpers/index.js"; import { animate, lerp, snapToGrid } from "../helpers/index.js";
import {
LinearSRGBColorSpace,
LinearToneMapping,
NoToneMapping,
SRGBColorSpace,
} from "three";
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import type { OrthographicCamera } from "three"; import type { OrthographicCamera } from "three";
import Background from "../background/Background.svelte"; import Background from "../background/Background.svelte";
import type { GraphManager } from "../graph-manager.js"; import type { GraphManager } from "../graph-manager.js";
import { onMount, setContext } from "svelte"; import { getContext, onMount, setContext } from "svelte";
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte"; import GraphView from "./GraphView.svelte";
import type { Node, Node as NodeType, Socket } from "@nodes/types"; import type { Node, Node as NodeType, Socket } from "@nodes/types";
@ -23,10 +17,15 @@
possibleSocketIds, possibleSocketIds,
selectedNodes, selectedNodes,
} from "./stores.js"; } from "./stores.js";
import { createKeyMap } from "../../helpers/createKeyMap";
import BoxSelection from "../BoxSelection.svelte"; import BoxSelection from "../BoxSelection.svelte";
import AddMenu from "../AddMenu.svelte"; import AddMenu from "../AddMenu.svelte";
export let graph: GraphManager; export let graph: GraphManager;
let keymap =
getContext<ReturnType<typeof createKeyMap>>("keymap") || createKeyMap([]);
setContext("graphManager", graph); setContext("graphManager", graph);
const status = graph.status; const status = graph.status;
const nodes = graph.nodes; const nodes = graph.nodes;
@ -78,19 +77,6 @@
localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition)); localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition));
} }
export let debug = {};
$: debug = {
activeNodeId: $activeNodeId,
activeSocket: $activeSocket
? `${$activeSocket?.node.id}-${$activeSocket?.index}`
: null,
hoveredSocket: $hoveredSocket
? `${$hoveredSocket?.node.id}-${$hoveredSocket?.index}`
: null,
selectedNodes: [...($selectedNodes?.values() || [])],
cameraPosition,
};
function updateNodePosition(node: NodeType) { function updateNodePosition(node: NodeType) {
if (node?.tmp?.ref) { if (node?.tmp?.ref) {
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) { if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
@ -518,30 +504,61 @@
$selectedNodes = new Set(newNodes.map((n) => n.id)); $selectedNodes = new Set(newNodes.map((n) => n.id));
} }
function handleKeyDown(event: KeyboardEvent) { const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
const bodyIsFocused = document?.activeElement?.nodeName !== "INPUT";
if (event.key === "l") { keymap.addShortcut({
key: "l",
description: "Select linked nodes",
callback: () => {
const activeNode = graph.getNode($activeNodeId); const activeNode = graph.getNode($activeNodeId);
if (activeNode) { if (activeNode) {
const nodes = graph.getLinkedNodes(activeNode); const nodes = graph.getLinkedNodes(activeNode);
$selectedNodes = new Set(nodes.map((n) => n.id)); $selectedNodes = new Set(nodes.map((n) => n.id));
} }
console.log(activeNode); console.log(activeNode);
} },
});
if (event.key === "Escape") { keymap.addShortcut({
key: "c",
ctrl: true,
description: "Copy active nodes",
callback: copyNodes,
});
keymap.addShortcut({
key: "v",
ctrl: true,
description: "Paste nodes",
callback: pasteNodes,
});
keymap.addShortcut({
key: "Escape",
description: "Deselect nodes",
callback: () => {
$activeNodeId = -1; $activeNodeId = -1;
$selectedNodes?.clear(); $selectedNodes?.clear();
$selectedNodes = $selectedNodes; $selectedNodes = $selectedNodes;
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
} },
});
if (event.key === "A" && event.shiftKey) { keymap.addShortcut({
key: "A",
shift: true,
description: "Add new Node",
callback: () => {
addMenuPosition = [mousePosition[0], mousePosition[1]]; addMenuPosition = [mousePosition[0], mousePosition[1]];
} },
});
keymap.addShortcut({
key: ".",
description: "Center camera",
callback: () => {
if (!isBodyFocused()) return;
if (event.key === "." && bodyIsFocused) {
const average = [0, 0]; const average = [0, 0];
for (const node of $nodes.values()) { for (const node of $nodes.values()) {
average[0] += node.position[0]; average[0] += node.position[0];
@ -564,40 +581,50 @@
); );
if (mouseDown) return false; if (mouseDown) return false;
}); });
} },
});
if (event.key === "a" && event.ctrlKey && bodyIsFocused) { keymap.addShortcut({
key: "a",
ctrl: true,
description: "Select all nodes",
callback: () => {
if (!isBodyFocused()) return;
$selectedNodes = new Set($nodes.keys()); $selectedNodes = new Set($nodes.keys());
} },
});
if (event.key === "c" && event.ctrlKey) { keymap.addShortcut({
copyNodes(); key: "z",
} ctrl: true,
description: "Undo",
if (event.key === "v" && event.ctrlKey) { callback: () => {
pasteNodes(); if (!isBodyFocused()) return;
}
if (event.key === "z" && event.ctrlKey) {
graph.undo(); graph.undo();
for (const node of $nodes.values()) { for (const node of $nodes.values()) {
updateNodePosition(node); updateNodePosition(node);
} }
} },
});
if (event.key === "y" && event.ctrlKey) { keymap.addShortcut({
key: "y",
ctrl: true,
description: "Redo",
callback: () => {
if (!isBodyFocused()) return;
graph.redo(); graph.redo();
for (const node of $nodes.values()) { for (const node of $nodes.values()) {
updateNodePosition(node); updateNodePosition(node);
} }
} },
});
if ( keymap.addShortcut({
(event.key === "Delete" || key: ["Delete", "Backspace", "x"],
event.key === "Backspace" || description: "Delete selected nodes",
event.key === "x") && callback: (event) => {
bodyIsFocused if (!isBodyFocused()) return;
) {
graph.startUndoGroup(); graph.startUndoGroup();
if ($activeNodeId !== -1) { if ($activeNodeId !== -1) {
const node = graph.getNode($activeNodeId); const node = graph.getNode($activeNodeId);
@ -617,8 +644,8 @@
$selectedNodes = $selectedNodes; $selectedNodes = $selectedNodes;
} }
graph.saveUndoGroup(); graph.saveUndoGroup();
} },
} });
function handleMouseUp(event: MouseEvent) { function handleMouseUp(event: MouseEvent) {
const activeNode = graph.getNode($activeNodeId); const activeNode = graph.getNode($activeNodeId);
@ -752,7 +779,7 @@
tabindex="0" tabindex="0"
bind:clientWidth={width} bind:clientWidth={width}
bind:clientHeight={height} bind:clientHeight={height}
on:keydown={handleKeyDown} on:keydown={keymap.handleKeyboardEvent}
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
> >
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}> <Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>

View File

@ -24,6 +24,8 @@ if ("getComputedStyle" in globalThis) {
const body = document.body; const body = document.body;
let lastStyle = "";
function updateColors() { function updateColors() {
const style = getComputedStyle(body); const style = getComputedStyle(body);
@ -35,6 +37,11 @@ if ("getComputedStyle" in globalThis) {
const active = style.getPropertyValue("--active"); const active = style.getPropertyValue("--active");
const selected = style.getPropertyValue("--selected"); const selected = style.getPropertyValue("--selected");
const newStyle = `${layer0}${layer1}${layer2}${layer3}${outline}${active}${selected}`;
if (newStyle === lastStyle) return;
lastStyle = newStyle;
colors.update(col => { colors.update(col => {
col.layer0.setStyle(layer0); col.layer0.setStyle(layer0);
col.layer0.convertLinearToSRGB(); col.layer0.convertLinearToSRGB();

View File

@ -26,8 +26,8 @@
const aspectRatio = 0.25; const aspectRatio = 0.25;
const path = createNodePath({ const path = createNodePath({
depth: 4, depth: 7,
height: 24, height: 40,
y: 50, y: 50,
cornerTop, cornerTop,
rightBump, rightBump,
@ -42,8 +42,8 @@
aspectRatio, aspectRatio,
}); });
const pathHover = createNodePath({ const pathHover = createNodePath({
depth: 5, depth: 9,
height: 30, height: 50,
y: 50, y: 50,
cornerTop, cornerTop,
rightBump, rightBump,

View File

@ -46,8 +46,8 @@
const aspectRatio = 0.5; const aspectRatio = 0.5;
const path = createNodePath({ const path = createNodePath({
depth: 4, depth: 7,
height: 12, height: 20,
y: 51, y: 51,
cornerBottom, cornerBottom,
leftBump, leftBump,
@ -62,8 +62,8 @@
aspectRatio, aspectRatio,
}); });
const pathHover = createNodePath({ const pathHover = createNodePath({
depth: 6, depth: 8,
height: 18, height: 25,
y: 50.5, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,

View File

@ -0,0 +1,43 @@
import { get, writable } from "svelte/store";
type Shortcut = {
key: string | string[],
shift?: boolean,
ctrl?: boolean,
alt?: boolean,
description?: string,
callback: (event: KeyboardEvent) => void
}
export function createKeyMap(keys: Shortcut[]) {
const store = writable(keys);
return {
handleKeyboardEvent: (event: KeyboardEvent) => {
const key = get(store).find(k => {
if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false;
if ("shift" in k && k.shift !== event.shiftKey) return false;
if ("ctrl" in k && k.ctrl !== event.ctrlKey) return false;
if ("alt" in k && k.alt !== event.altKey) return false;
return true;
});
console.log({ keys: get(store), out: key, key: event.key });
key?.callback(event);
},
addShortcut: (shortcut: Shortcut) => {
if (Array.isArray(shortcut.key)) {
for (const k of shortcut.key) {
store.update(keys => {
if (keys.find(kk => kk.key === k)) return keys;
return [...keys, { ...shortcut, key: k }];
});
}
} else {
store.update(keys => [...keys, shortcut]);
}
},
keys: store
}
}

View File

@ -1,4 +1,75 @@
<script lang="ts"> <script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { getContext } from "svelte";
const { keys } = getContext<ReturnType<typeof createKeyMap>>("keymap");
</script> </script>
duuude <div class="wrapper">
<h3>Editor</h3>
<section>
{#each $keys as key}
{#if key.description}
<div class="command-wrapper">
<div class="command">
{#if key.ctrl}
<b>Ctrl</b>
{/if}
{#if key.shift}
<b>Shift</b>
{/if}
{#if key.alt}
<b>Alt</b>
{/if}
<b>{key.key}</b>
</div>
</div>
<p>{key.description}</p>
{/if}
{/each}
</section>
</div>
<style>
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
section {
display: grid;
grid-template-columns: min-content 1fr;
gap: 1em;
}
h3 {
margin: 0;
}
.command-wrapper {
display: flex;
justify-content: left;
align-items: center;
}
.command {
background: var(--layer-3);
padding: 0.4em;
border-radius: 0.3em;
white-space: nowrap;
}
.command > * {
color: var(--layer-0);
}
p {
font-size: 0.9em;
margin: 0;
display: flex;
align-items: center;
}
</style>

View File

@ -53,7 +53,6 @@
} }
details { details {
padding: 1rem; padding: 1rem;
min-height: 69px;
box-sizing: border-box; box-sizing: border-box;
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--outline);
} }

View File

@ -10,6 +10,8 @@
import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings"; import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import Keymap from "$lib/settings/Keymap.svelte"; import Keymap from "$lib/settings/Keymap.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap";
import { setContext } from "svelte";
const nodeRegistry = new RemoteNodeRegistry("http://localhost:3001"); const nodeRegistry = new RemoteNodeRegistry("http://localhost:3001");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry); const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
@ -30,6 +32,10 @@
localStorage.setItem("graph", JSON.stringify(event.detail)); localStorage.setItem("graph", JSON.stringify(event.detail));
} }
const keyMap = createKeyMap([]);
setContext("keymap", keyMap);
let settings: Record<string, any> = { let settings: Record<string, any> = {
general: { general: {
id: "general", id: "general",