feat: add outline to themes

This commit is contained in:
max_richter 2024-04-19 02:36:11 +02:00
parent d8ada83db3
commit c62cfbf75e
15 changed files with 315 additions and 277 deletions

View File

@ -1,164 +1,163 @@
<script lang="ts">
import type { GraphManager } from './graph-manager.js';
import { HTML } from '@threlte/extras';
import { onMount } from 'svelte';
import type { GraphManager } from "./graph-manager.js";
import { HTML } from "@threlte/extras";
import { onMount } from "svelte";
export let position: [x: number, y: number] | null;
export let position: [x: number, y: number] | null;
export let graph: GraphManager;
export let graph: GraphManager;
let input: HTMLInputElement;
let value: string = '';
let activeNodeId: string = '';
let input: HTMLInputElement;
let value: string = "";
let activeNodeId: string = "";
const allNodes = graph.getNodeTypes();
const allNodes = graph.getNodeTypes();
function filterNodes() {
return allNodes.filter((node) => node.id.includes(value));
}
function filterNodes() {
return allNodes.filter((node) => node.id.includes(value));
}
$: nodes = value === '' ? allNodes : filterNodes();
$: if (nodes) {
if (activeNodeId === '') {
activeNodeId = nodes[0].id;
} else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId);
if (!node) {
activeNodeId = nodes[0].id;
}
}
}
$: nodes = value === "" ? allNodes : filterNodes();
$: if (nodes) {
if (activeNodeId === "") {
activeNodeId = nodes[0].id;
} else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId);
if (!node) {
activeNodeId = nodes[0].id;
}
}
}
function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation();
const value = (event.target as HTMLInputElement).value;
function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation();
if (event.key === 'Escape') {
position = null;
return;
}
if (event.key === "Escape") {
position = null;
return;
}
if (event.key === 'ArrowDown') {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index + 1) % nodes.length].id;
return;
}
if (event.key === "ArrowDown") {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index + 1) % nodes.length].id;
return;
}
if (event.key === 'ArrowUp') {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
return;
}
if (event.key === "ArrowUp") {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
return;
}
if (event.key === 'Enter') {
if (activeNodeId && position) {
graph.createNode({ type: activeNodeId, position });
position = null;
}
return;
}
}
if (event.key === "Enter") {
if (activeNodeId && position) {
graph.createNode({ type: activeNodeId, position });
position = null;
}
return;
}
}
onMount(() => {
input.disabled = false;
setTimeout(() => input.focus(), 50);
});
onMount(() => {
input.disabled = false;
setTimeout(() => input.focus(), 50);
});
</script>
<HTML position.x={position?.[0]} position.z={position?.[1]} transform={false}>
<div class="add-menu-wrapper">
<div class="header">
<input
id="add-menu"
type="text"
aria-label="Search for a node type"
role="searchbox"
placeholder="Search..."
disabled={false}
on:keydown={handleKeyDown}
bind:value
bind:this={input}
/>
</div>
<div class="add-menu-wrapper">
<div class="header">
<input
id="add-menu"
type="text"
aria-label="Search for a node type"
role="searchbox"
placeholder="Search..."
disabled={false}
on:keydown={handleKeyDown}
bind:value
bind:this={input}
/>
</div>
<div class="content">
{#each nodes as node}
<div
class="result"
role="treeitem"
tabindex="0"
aria-selected={node.id === activeNodeId}
on:keydown={(event) => {
if (event.key === 'Enter') {
if (position) {
graph.createNode({ type: node.id, position });
position = null;
}
}
}}
on:mousedown={() => {
if (position) {
graph.createNode({ type: node.id, position });
position = null;
}
}}
on:focus={() => {
activeNodeId = node.id;
}}
class:selected={node.id === activeNodeId}
on:mouseover={() => {
activeNodeId = node.id;
}}
>
{node.id}
</div>
{/each}
</div>
</div>
<div class="content">
{#each nodes as node}
<div
class="result"
role="treeitem"
tabindex="0"
aria-selected={node.id === activeNodeId}
on:keydown={(event) => {
if (event.key === "Enter") {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
}
}}
on:mousedown={() => {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
}}
on:focus={() => {
activeNodeId = node.id;
}}
class:selected={node.id === activeNodeId}
on:mouseover={() => {
activeNodeId = node.id;
}}
>
{node.id}
</div>
{/each}
</div>
</div>
</HTML>
<style>
input {
background: var(--background-color-lighter);
font-family: var(--font-family);
border: none;
color: var(--text-color);
padding: 0.8em;
width: calc(100% - 2px);
box-sizing: border-box;
font-size: 1em;
margin-left: 1px;
margin-top: 1px;
}
input {
background: var(--layer-0);
font-family: var(--font-family);
border: none;
color: var(--text-color);
padding: 0.8em;
width: calc(100% - 2px);
box-sizing: border-box;
font-size: 1em;
margin-left: 1px;
margin-top: 1px;
}
input:focus {
outline: solid 2px rgba(255, 255, 255, 0.2);
}
input:focus {
outline: solid 2px rgba(255, 255, 255, 0.2);
}
.add-menu-wrapper {
position: absolute;
background: var(--background-color);
border-radius: 7px;
overflow: hidden;
border: solid 2px var(--background-color-lighter);
width: 150px;
}
.content {
min-height: none;
width: 100%;
color: var(--text-color);
}
.add-menu-wrapper {
position: absolute;
background: var(--layer-1);
border-radius: 7px;
overflow: hidden;
border: solid 2px var(--layer-2);
width: 150px;
}
.content {
min-height: none;
width: 100%;
color: var(--text-color);
}
.result {
padding: 1em 0.9em;
border-bottom: solid 1px var(--background-color-lighter);
opacity: 0.7;
font-size: 0.9em;
cursor: pointer;
}
.result {
padding: 1em 0.9em;
border-bottom: solid 1px var(--layer-2);
opacity: 0.7;
font-size: 0.9em;
cursor: pointer;
}
.result[aria-selected='true'] {
background: var(--background-color-lighter);
opacity: 1;
}
.result[aria-selected="true"] {
background: var(--layer-2);
opacity: 1;
}
</style>

View File

@ -1,28 +1,31 @@
<script lang="ts">
import { HTML } from '@threlte/extras';
import { HTML } from "@threlte/extras";
export let p1 = { x: 0, y: 0 };
export let p2 = { x: 0, y: 0 };
export let p1 = { x: 0, y: 0 };
export let p2 = { x: 0, y: 0 };
export let cameraPosition = [0, 1, 0];
export let cameraPosition = [0, 1, 0];
$: width = Math.abs(p1.x - p2.x) * cameraPosition[2];
$: height = Math.abs(p1.y - p2.y) * cameraPosition[2];
$: width = Math.abs(p1.x - p2.x) * cameraPosition[2];
$: height = Math.abs(p1.y - p2.y) * cameraPosition[2];
$: x = Math.max(p1.x, p2.x) - width / cameraPosition[2];
$: y = Math.max(p1.y, p2.y) - height / cameraPosition[2];
$: x = Math.max(p1.x, p2.x) - width / cameraPosition[2];
$: y = Math.max(p1.y, p2.y) - height / cameraPosition[2];
</script>
<HTML position.x={x} position.z={y} transform={false}>
<div class="box-selection" style={`width: ${width}px; height: ${height}px;`}></div>
<div
class="box-selection"
style={`width: ${width}px; height: ${height}px;`}
></div>
</HTML>
<style>
.box-selection {
width: 40px;
height: 20px;
border: solid 0.2px rgba(200, 200, 200, 0.8);
border-style: dashed;
border-radius: 2px;
}
.box-selection {
width: 40px;
height: 20px;
border: solid 0.2px var(--outline);
border-style: dashed;
border-radius: 2px;
}
</style>

View File

@ -746,7 +746,7 @@
<div
on:wheel={handleMouseScroll}
bind:this={wrapper}
class="wrapper"
class="graph-wrapper"
aria-label="Graph"
role="button"
tabindex="0"
@ -794,8 +794,9 @@
</div>
<style>
.wrapper {
.graph-wrapper {
position: relative;
transition: opacity 0.3s ease;
height: 100%;
}
</style>

View File

@ -12,6 +12,8 @@
const manager = new GraphManager(registry);
export const status = manager.status;
const updateSettings = debounce((s) => {
manager.setSettings(s);
}, 200);

View File

@ -16,6 +16,8 @@ export const colors = writable({
layer2: new Color().setStyle("#2D2D2D"),
layer3: new Color().setStyle("#A6A6A6"),
outline: new Color().setStyle("#000000"),
active: new Color().setStyle("#c65a19"),
selected: new Color().setStyle("#ffffff"),
});
if ("getComputedStyle" in globalThis) {
@ -30,6 +32,8 @@ if ("getComputedStyle" in globalThis) {
const layer2 = style.getPropertyValue("--layer-2");
const layer3 = style.getPropertyValue("--layer-3");
const outline = style.getPropertyValue("--outline");
const active = style.getPropertyValue("--active");
const selected = style.getPropertyValue("--selected");
colors.update(col => {
col.layer0.setStyle(layer0);
@ -42,6 +46,10 @@ if ("getComputedStyle" in globalThis) {
col.layer3.convertLinearToSRGB();
col.outline.setStyle(outline);
col.outline.convertLinearToSRGB();
col.active.setStyle(active);
col.active.convertLinearToSRGB();
col.selected.setStyle(selected);
col.selected.convertLinearToSRGB();
return col;
});

View File

@ -6,12 +6,8 @@ uniform float uHeight;
uniform vec3 uColorDark;
uniform vec3 uColorBright;
uniform vec3 uSelectedColor;
uniform vec3 uActiveColor;
uniform bool uSelected;
uniform bool uActive;
uniform vec3 uStrokeColor;
uniform float uStrokeWidth;
float msign(in float x) { return (x < 0.0) ? -1.0 : 1.0; }
@ -47,16 +43,9 @@ void main(){
// outside
gl_FragColor = vec4(0.0,0.0,0.0, 0.0);
}else{
if (distance.w > -uStrokeWidth || mod(y+5.0, 10.0) < uStrokeWidth/2.0) {
// draw the outer stroke
if (uSelected) {
gl_FragColor = vec4(uSelectedColor, 1.0);
} else if (uActive) {
gl_FragColor = vec4(uActiveColor, 1.0);
} else {
gl_FragColor = vec4(uColorBright, 1.0);
}
gl_FragColor = vec4(uStrokeColor, 1.0);
}else if (y<5.0){
// draw the header
gl_FragColor = vec4(uColorBright, 1.0);

View File

@ -67,18 +67,18 @@
uniforms={{
uColorBright: { value: new Color("#171717") },
uColorDark: { value: new Color("#151515") },
uSelectedColor: { value: new Color("#9d5f28") },
uActiveColor: { value: new Color("white") },
uSelected: { value: false },
uActive: { value: false },
uStrokeColor: { value: new Color("#9d5f28") },
uStrokeWidth: { value: 1.0 },
uWidth: { value: 20 },
uHeight: { value: height },
}}
uniforms.uSelected.value={isSelected}
uniforms.uActive.value={isActive}
uniforms.uColorBright.value={$colors.layer2}
uniforms.uColorDark.value={$colors.layer1}
uniforms.uStrokeColor.value={isSelected
? $colors.selected
: isActive
? $colors.active
: $colors.outline}
uniforms.uStrokeWidth.value={(7 - z) / 3}
/>
</T.Mesh>
@ -122,12 +122,12 @@
}
.node.active {
--stroke: white;
--stroke-width: 1px;
--stroke: var(--active);
--stroke-width: 2px;
}
.node.selected {
--stroke: #9d5f28;
--stroke-width: 1px;
--stroke: var(--selected);
--stroke-width: 2px;
}
</style>

View File

@ -0,0 +1,4 @@
<script lang="ts">
</script>
duuude

View File

@ -11,7 +11,6 @@
export let store: Writable<Record<string, any>>;
export let depth = 0;
console.log(settings);
const keys = Object.keys(settings);
function isNodeInput(v: NodeInput | Nested): v is NodeInput {
return "type" in v;
@ -23,12 +22,18 @@
<div class="wrapper" class:first-level={depth === 0}>
{#if isNodeInput(value)}
<div class="input input-{settings[key].type}">
<label for={key}>{settings[key].label || key}</label>
<Input
id={key}
input={value}
bind:value={$store[value?.setting || key]}
/>
{#if settings[key].type === "button"}
<button on:click={() => settings[key]?.callback?.()}
>{key || settings[key].label}</button
>
{:else}
<label for={key}>{settings[key].label || key}</label>
<Input
id={key}
input={value}
bind:value={$store[value?.setting || key]}
/>
{/if}
</div>
{:else}
<details>

View File

@ -1,49 +0,0 @@
<script lang="ts">
import type { NodeInput } from "@nodes/types";
import type { Writable } from "svelte/store";
import NestedSettings from "./NestedSettings.svelte";
export let setting: {
icon: string;
id: string;
definition: Record<string, NodeInput>;
settings: Writable<Record<string, unknown>>;
};
const store = setting.settings;
interface Nested {
[key: string]: NodeInput | Nested;
}
$: nestedSettings = constructNested();
function constructNested() {
const nested: Nested = {};
for (const key in setting.definition) {
const parts = key.split(".");
let current = nested;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = setting.definition[key];
} else {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]] as Nested;
}
}
}
return nested;
}
</script>
<div class="flex flex-col">
<h1 class="m-0 p-4">{setting.id}</h1>
<NestedSettings settings={nestedSettings} {store} />
</div>
<style>
h1 {
border-bottom: solid thin var(--outline);
}
</style>

View File

@ -1,11 +1,11 @@
<script lang="ts">
import type { NodeInput } from "@nodes/types";
import type { Writable } from "svelte/store";
import SettingsComponent from "./Panel.svelte";
import localStore from "$lib/helpers/localStore";
import type { SvelteComponent } from "svelte";
import NestedSettings from "./NestedSettings.svelte";
export let settings: Record<
export let panels: Record<
string,
{
icon: string;
@ -16,13 +16,15 @@
}
>;
const activePanel = localStore<keyof typeof settings | false>(
const activePanel = localStore<keyof typeof panels | false>(
"nodes.settings.activePanel",
false,
);
$: keys = Object.keys(settings) as unknown as (keyof typeof settings)[];
$: keys = panels
? (Object.keys(panels) as unknown as (keyof typeof panels)[])
: [];
function setActivePanel(panel: keyof typeof settings | false) {
function setActivePanel(panel: keyof typeof panels | false) {
if (panel === $activePanel) {
$activePanel = false;
} else if (panel) {
@ -31,6 +33,28 @@
$activePanel = false;
}
}
interface Nested {
[key: string]: NodeInput | Nested;
}
function constructNested(panel: (typeof panels)[keyof typeof panels]) {
const nested: Nested = {};
for (const key in panel.definition) {
const parts = key.split(".");
let current = nested;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = panel.definition[key];
} else {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]] as Nested;
}
}
}
return nested;
}
</script>
<div class="wrapper" class:visible={$activePanel}>
@ -42,26 +66,32 @@
>
<span class="absolute i-tabler-chevron-left w-6 h-6 block"></span>
</button>
{#each keys as panel (settings[panel].id)}
{#each keys as panel (panels[panel].id)}
<button
class="tab"
class:active={panel === $activePanel}
on:click={() => setActivePanel(panel)}
>
<i class={`block w-6 h-6 ${settings[panel].icon}`} />
<i class={`block w-6 h-6 ${panels[panel].icon}`} />
</button>
{/each}
</div>
<div class="content">
{#if $activePanel && settings[$activePanel]}
{#if $activePanel && panels[$activePanel]}
<h1 class="m-0 p-4">{panels[$activePanel].id}</h1>
{#key $activePanel}
{#if settings[$activePanel].component}
{#if panels[$activePanel]?.component}
<svelte:component
this={settings[$activePanel].component}
{...settings[$activePanel]}
this={panels[$activePanel].component}
{...panels[$activePanel]}
/>
{:else}
<SettingsComponent setting={settings[$activePanel]} />
<div class="flex flex-col">
<NestedSettings
settings={constructNested(panels[$activePanel])}
store={panels[$activePanel].settings}
/>
</div>
{/if}
{/key}
{/if}
@ -84,6 +114,10 @@
min-width: 300px;
}
h1 {
border-bottom: solid thin var(--outline);
}
.content {
background: var(--layer-1);
}
@ -97,6 +131,7 @@
.tabs > button {
height: 30px;
padding: 5px;
border-radius: 0px;
background: none;
color: var(--outline);
border: none;

View File

@ -33,6 +33,19 @@ export const AppSettingTypes = {
label: "Show Grid",
value: true,
},
stressTest: {
amount: {
type: "integer",
min: 2,
max: 15
},
loadGrid: {
type: "button"
},
loadTree: {
type: "button"
},
},
debug: {
wireframe: {
type: "boolean",

View File

@ -8,7 +8,8 @@
import Viewer from "$lib/viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte";
import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings";
import { get, writable } from "svelte/store";
import { get, writable, type Writable } from "svelte/store";
import Keymap from "$lib/settings/Keymap.svelte";
const nodeRegistry = new RemoteNodeRegistry("http://localhost:3001");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
@ -19,6 +20,8 @@
? JSON.parse(localStorage.getItem("graph")!)
: templates.grid(3, 3);
let managerStatus: Writable<"loading" | "error" | "idle">;
function handleResult(event: CustomEvent<Graph>) {
res = runtimeExecutor.execute(event.detail, get(settings?.graph?.settings));
}
@ -34,6 +37,12 @@
settings: AppSettings,
definition: AppSettingTypes,
},
graph: {},
shortcuts: {
id: "shortcuts",
icon: "i-tabler-keyboard",
component: Keymap,
},
};
function handleSettings(
@ -42,27 +51,28 @@
types: Record<string, unknown>;
}>,
) {
settings = {
...settings,
graph: {
icon: "i-tabler-chart-bar",
id: "graph",
settings: writable(ev.detail.values),
definition: ev.detail.types,
},
settings.general.definition.stressTest.loadGrid.callback = function () {
const store = get(settings.general.settings);
graph = templates.grid(store.amount, store.amount);
};
settings.general.definition.stressTest.loadTree.callback = function () {
const store = get(settings.general.settings);
graph = templates.tree(store.amount);
};
settings.graph = {
icon: "i-tabler-chart-bar",
id: "graph",
settings: writable(ev.detail.values),
definition: ev.detail.types,
};
settings = settings;
}
</script>
<div class="wrapper">
<header>
header
<button
on:click={() => {
graph = templates.grid(15, 15);
}}>grid stress-test</button
>
</header>
<div class="wrapper manager-{$managerStatus}">
<header></header>
<Grid.Row>
<Grid.Cell>
<Viewer result={res} />
@ -72,12 +82,13 @@
<GraphInterface
registry={nodeRegistry}
{graph}
bind:status={managerStatus}
settings={settings?.graph?.settings}
on:settings={handleSettings}
on:result={handleResult}
on:save={handleSave}
/>
<Settings {settings}></Settings>
<Settings panels={settings}></Settings>
{/key}
</Grid.Cell>
</Grid.Row>
@ -97,6 +108,17 @@
grid-template-rows: 50px 1fr;
}
.wrapper :global(canvas) {
transition: opacity 0.3s ease;
opacity: 1;
}
.manager-loading :global(.graph-wrapper),
.manager-loading :global(canvas) {
opacity: 0.2;
pointer-events: none;
}
:global(html) {
background: rgb(13, 19, 32);
background: linear-gradient(

View File

View File

@ -57,6 +57,9 @@ body {
--layer-2: var(--neutral-400);
--layer-3: var(--neutral-200);
--active: #ffffff;
--selected: #c65a19;
--outline: var(--neutral-400);
--text-color: var(--neutral-200);
@ -76,6 +79,8 @@ body.theme-light {
--layer-1: var(--neutral-100);
--layer-2: var(--neutral-100);
--layer-3: var(--neutral-500);
--active: #000000;
--selected: #c65a19;
}
body.theme-solarized {
@ -83,8 +88,10 @@ body.theme-solarized {
--outline: #93a1a1;
--layer-0: #fdf6e3;
--layer-1: #eee8d5;
--layer-2: #93a1a1;
--layer-2: #c4c0b4;
--layer-3: #586e75;
--active: #000000;
--selected: #268bd2;
}
body.theme-catppuccin {
@ -107,17 +114,13 @@ body.theme-high-contrast {
body.theme-nord {
--text-color: #D8DEE9;
/* Nord snow */
--outline: #4C566A;
/* Nord frost */
--layer-0: #2E3440;
/* Nord polar night */
--layer-1: #3B4252;
/* Nord polar night */
--layer-2: #434C5E;
/* Nord polar night */
--layer-3: #5E81AC;
/* Nord frost */
--active: #8999bd;
--selected: #b76c3f
}
body.theme-dracula {
@ -133,6 +136,9 @@ body {
margin: 0;
}
/* canvas { */
/* display: none !important; */
/* } */
button {
background-color: var(--layer-2);
border: 1px solid var(--outline);
padding: 8px 9px;
border-radius: 4px;
}