Compare commits
102 Commits
1126cf8f9f
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
fdc4802d68
|
|||
|
5a30fce4ec
|
|||
|
0d0eb65ddd
|
|||
|
93ca436412
|
|||
|
ecdb986a96
|
|||
|
304abf2866
|
|||
|
a547d86946
|
|||
|
667d140883
|
|||
|
0ac65fd7a7
|
|||
|
|
5437e062e1
|
||
|
|
1015d17afb
|
||
|
|
fd8e5e92d2
|
||
| a2c2503a8e | |||
|
|
e18f75e1b8
|
||
|
|
6a178dc3a7
|
||
|
|
76cdfee018
|
||
|
|
b19da950a6
|
||
|
|
89e4cf8364
|
||
|
|
a28f15c256
|
||
|
|
57e3a707c5
|
||
|
|
dced7db3ad
|
||
|
|
c2dc538c05
|
||
|
|
9484b3599e
|
||
|
|
3c168aa9b6
|
||
|
|
812099c55d
|
||
|
|
025921aeab
|
||
|
|
abaf5245d3
|
||
|
|
a53cee2d5c
|
||
|
|
7d91e53704
|
||
|
|
18db3cb9f2
|
||
|
|
b6d3269478
|
||
| 7d18c10007 | |||
|
|
6f33cdf066
|
||
|
|
30e897468a
|
||
|
|
137425b31b
|
||
|
|
5570d975f5 | ||
|
|
8c1ba2ee65 | ||
|
|
3e019e4e21 | ||
|
|
a58b19e935 | ||
|
|
6f5c5bb46e
|
||
|
7f2214f15c
|
|||
|
43ef563ae7
|
|||
|
|
714d01da94 | ||
|
|
92308fc43a | ||
|
|
5adf67ed52 | ||
|
|
f54cde734e | ||
| 70d8095869 | |||
|
|
2a90d5de3f | ||
|
d7e9e8b8de
|
|||
|
bdbaab25a4
|
|||
|
|
c7bfb0f05b | ||
|
|
d3a46af4c2 | ||
|
4c76c62a3e
|
|||
|
36cf9211d2
|
|||
|
97a2ffb683
|
|||
|
fffa8c7cdf
|
|||
| de799c2d55 | |||
| 24bef0460c | |||
|
93b64fc7dd
|
|||
|
64ac28f60c
|
|||
|
bd0c2eaacd
|
|||
|
8693c63d16
|
|||
| fbd82bbdfa | |||
| 63997ec262 | |||
|
a3d10d6094
|
|||
|
|
6b6038e546 | ||
|
|
3e3d41ae98 | ||
|
|
a8c76a846e | ||
|
f98b90dcd3
|
|||
|
1ea7d6629f
|
|||
|
617dfb0c9d
|
|||
|
|
c46bf9e64f | ||
|
|
0cfd1e5c96 | ||
|
|
ecfd4d5f2f | ||
|
|
03102fdc75 | ||
|
|
0bd00b0192 | ||
|
|
edbc71ed8f | ||
|
|
4485cef82b | ||
| cdef71265e | |||
|
8e5412c25c
|
|||
|
2904c13c41
|
|||
|
450262b4ae
|
|||
|
11de746c01
|
|||
|
83cb2bd950
|
|||
| e84c715f4c | |||
| f5cea555cd | |||
|
|
ecbcc814ed
|
||
|
|
be97387252
|
||
|
|
987ece2a4b
|
||
|
|
8d2e3f006b
|
||
|
|
80d3e117b4
|
||
|
|
8a540522dd
|
||
|
|
a11214072f
|
||
|
d068828b68
|
|||
|
3565a18364
|
|||
|
73be4fdd73
|
|||
|
702c3ee6cf
|
|||
|
98672eb702
|
|||
|
3eafdc50b1
|
|||
|
|
548e445eb7
|
||
| db77a4fd94 | |||
|
|
7ae1fae3b9
|
65
.dprint.jsonc
Normal file
65
.dprint.jsonc
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"$schema": "https://dprint.dev/schemas/v0.json",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100,
|
||||
"typescript": {
|
||||
// https://dprint.dev/plugins/typescript/config/
|
||||
"quoteStyle": "preferSingle",
|
||||
"trailingCommas": "never",
|
||||
},
|
||||
"json": {
|
||||
// https://dprint.dev/plugins/json/config/
|
||||
},
|
||||
"markdown": {},
|
||||
"toml": {},
|
||||
"dockerfile": {},
|
||||
"ruff": {},
|
||||
"jupyter": {},
|
||||
"malva": {},
|
||||
"markup": {
|
||||
// https://dprint.dev/plugins/markup_fmt/config/
|
||||
"scriptIndent": true,
|
||||
"styleIndent": true,
|
||||
},
|
||||
"yaml": {},
|
||||
"graphql": {},
|
||||
"exec": {
|
||||
"cwd": "${configDir}",
|
||||
"commands": [
|
||||
{
|
||||
"command": "rustfmt",
|
||||
"exts": [
|
||||
"rs",
|
||||
],
|
||||
"cacheKeyFiles": [
|
||||
"rustfmt.toml",
|
||||
"rust-toolchain.toml",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"excludes": [
|
||||
"**/node_modules",
|
||||
"**/build",
|
||||
"**/.svelte-kit",
|
||||
"**/package",
|
||||
"**/*-lock.yaml",
|
||||
"**/yaml.lock",
|
||||
"**/.DS_Store",
|
||||
],
|
||||
"plugins": [
|
||||
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
|
||||
"https://plugins.dprint.dev/json-0.21.1.wasm",
|
||||
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
|
||||
"https://plugins.dprint.dev/toml-0.7.0.wasm",
|
||||
"https://plugins.dprint.dev/dockerfile-0.3.3.wasm",
|
||||
"https://plugins.dprint.dev/ruff-0.6.11.wasm",
|
||||
"https://plugins.dprint.dev/jupyter-0.2.1.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/malva-v0.15.1.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/pretty_graphql-v0.2.3.wasm",
|
||||
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364",
|
||||
"https://plugins.dprint.dev/biome-0.11.10.wasm",
|
||||
],
|
||||
}
|
||||
472
.gitea/graphics/nodes.svg
Normal file
472
.gitea/graphics/nodes.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 39 KiB |
77
.gitea/scripts/create-release.sh
Executable file
77
.gitea/scripts/create-release.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
TAG="$GITHUB_REF_NAME"
|
||||
VERSION=$(echo "$TAG" | sed 's/^v//')
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
|
||||
echo "🚀 Creating release for $TAG (safe mode)"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 1. Extract release notes from annotated tag
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
NOTES=$(git tag -l "$TAG" --format='%(contents)')
|
||||
|
||||
if [ -z "$NOTES" ]; then
|
||||
echo "❌ Tag message is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout main
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 2. Update all package.json versions
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
echo "🔧 Updating package.json versions to $VERSION"
|
||||
|
||||
find . -name package.json ! -path "*/node_modules/*" | while read file; do
|
||||
tmp_file="$file.tmp"
|
||||
jq --arg v "$VERSION" '.version = $v' "$file" >"$tmp_file"
|
||||
mv "$tmp_file" "$file"
|
||||
done
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 3. Update CHANGELOG.md (prepend)
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
tmp_changelog="CHANGELOG.tmp"
|
||||
{
|
||||
echo "## $TAG ($DATE)"
|
||||
echo ""
|
||||
echo "$NOTES"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
cat CHANGELOG.md
|
||||
fi
|
||||
} >"$tmp_changelog"
|
||||
|
||||
mv "$tmp_changelog" CHANGELOG.md
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 4. Create release commit
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
git config user.name "release-bot"
|
||||
git config user.email "release-bot@ci"
|
||||
|
||||
git add CHANGELOG.md $(find . -name package.json ! -path "*/node_modules/*")
|
||||
|
||||
# Skip commit if nothing changed
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit for release $TAG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore(release): $TAG"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 5. Push changes
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
git push origin main
|
||||
|
||||
echo "✅ Release commit for $TAG created successfully (tag untouched)"
|
||||
58
.gitea/workflows/release.yaml
Normal file
58
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
name: 🚀 Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["*"]
|
||||
tags: ["*"]
|
||||
|
||||
env:
|
||||
PNPM_CACHE_FOLDER: ~/.pnpm-store
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
|
||||
steps:
|
||||
- name: 📑 Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 💾 Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.PNPM_CACHE_FOLDER }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
|
||||
|
||||
- name: 🧹 Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: 🎨 Format Check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: 🧬 Type Check
|
||||
run: pnpm check
|
||||
|
||||
- name: 🛠️ Build
|
||||
run: pnpm build:deploy
|
||||
|
||||
- name: 🚀 Create Release Commit
|
||||
if: github.ref_type == 'tag'
|
||||
run: ./.gitea/scripts/create-release.sh
|
||||
|
||||
- name: 🏷️ Create Gitea Release
|
||||
if: github.ref_type == 'tag'
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
release_name: Release ${{ github.ref_name }}
|
||||
body_path: CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
1
.github/graphics/nodes.svg
vendored
1
.github/graphics/nodes.svg
vendored
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 36 KiB |
38
.github/workflows/deploy.yaml
vendored
38
.github/workflows/deploy.yaml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: "main"
|
||||
|
||||
jobs:
|
||||
build_site:
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: build
|
||||
run: pnpm run build:deploy
|
||||
|
||||
- name: 🔑 Configure rclone
|
||||
run: |
|
||||
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
|
||||
chmod 600 /tmp/id_rsa
|
||||
mkdir -p ~/.config/rclone
|
||||
echo -e "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||
SSH_USER: ${{ vars.SSH_USER }}
|
||||
|
||||
- name: 🚀 Deploy Changed Files via rclone
|
||||
run: |
|
||||
echo "Uploading the rest"
|
||||
rclone sync --update -v --progress --exclude _astro/** --stats 2s --stats-one-line ./app/build/ sftp-remote:${REMOTE_DIR} --transfers 4
|
||||
env:
|
||||
REMOTE_DIR: ${{ vars.REMOTE_DIR }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules/
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
.direnv/
|
||||
|
||||
367
Cargo.lock
generated
367
Cargo.lock
generated
@@ -1,176 +1,80 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "box"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "branch"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.27.0"
|
||||
version = "0.30.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
|
||||
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
|
||||
|
||||
[[package]]
|
||||
name = "gravity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"noise",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instance"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "math"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "max-plantarium-triangle"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "max-plantarium-vec3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodarium_instance"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "nodarium_macros"
|
||||
@@ -180,7 +84,7 @@ dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn 1.0.109",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -195,29 +99,19 @@ dependencies = [
|
||||
name = "nodarium_utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"noise",
|
||||
"noise 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodes-noise"
|
||||
name = "noise"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"noise",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
"noise 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -233,48 +127,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "output"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
version = "1.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -307,103 +188,76 @@ dependencies = [
|
||||
name = "random"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rotate"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"glam",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.198"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-wasm-bindgen"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.198"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stem"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -411,119 +265,30 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||
name = "triangle"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||
name = "vec3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.92"
|
||||
name = "zmij"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
"scoped-tls",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -1,21 +1,19 @@
|
||||
FROM node:24-alpine
|
||||
|
||||
RUN apk add --no-cache --update curl rclone g++
|
||||
|
||||
RUN RUSTUP_URL="https://sh.rustup.rs" \
|
||||
&& curl --silent --show-error --location --fail --retry 3 --proto '=https' --tlsv1.2 --output /tmp/rustup-linux-install.sh $RUSTUP_URL \
|
||||
&& sh /tmp/rustup-linux-install.sh -y
|
||||
# Install all required packages in one layer
|
||||
RUN apk add --no-cache curl git jq g++ make
|
||||
|
||||
# Set Rust paths
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH
|
||||
|
||||
# Install Rust, wasm target, and pnpm
|
||||
RUN curl --silent --show-error --location --fail --retry 3 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
--output /tmp/rustup-init.sh https://sh.rustup.rs \
|
||||
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
|
||||
&& rm /tmp/rustup-init.sh \
|
||||
&& rustup default stable \
|
||||
&& rustup target add wasm32-unknown-unknown \
|
||||
&& cargo install wasm-pack \
|
||||
&& npm i -g pnpm
|
||||
--proto '=https' --tlsv1.2 \
|
||||
--output /tmp/rustup-init.sh https://sh.rustup.rs \
|
||||
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
|
||||
&& rm /tmp/rustup-init.sh \
|
||||
&& rustup target add wasm32-unknown-unknown \
|
||||
&& rm -rf /usr/local/rustup/toolchains/*/share/doc \
|
||||
&& npm i -g pnpm
|
||||
|
||||
37
README.md
37
README.md
@@ -2,17 +2,17 @@ Nodarium
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a>
|
||||
<a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a>
|
||||
|
||||
<p align="center">
|
||||
Nodarium is a WebAssembly based visual programming language.
|
||||
<p align="center">
|
||||
Nodarium is a WebAssembly based visual programming language.
|
||||
</p>
|
||||
|
||||
<img src=".github/graphics/nodes.svg" width="80%"/>
|
||||
<img src=".gitea/graphics/nodes.svg" width="80%"/>
|
||||
|
||||
</div>
|
||||
|
||||
Currently this visual programming language is used to develop https://nodes.max-richter.dev, a procedural modelling tool for 3d-plants.
|
||||
Currently this visual programming language is used to develop <https://nodes.max-richter.dev>, a procedural modelling tool for 3d-plants.
|
||||
|
||||
# Table of contents
|
||||
|
||||
@@ -22,7 +22,7 @@ Currently this visual programming language is used to develop https://nodes.max-
|
||||
|
||||
# Developing
|
||||
|
||||
### Install prerequisites:
|
||||
### Install prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- [pnpm](https://pnpm.io/installation)
|
||||
@@ -50,4 +50,29 @@ pnpm dev
|
||||
|
||||
### [Now you can create your first node 🤓](./docs/DEVELOPING_NODES.md)
|
||||
|
||||
# Releasing
|
||||
|
||||
## Creating a Release
|
||||
|
||||
1. **Create an annotated tag** with your release notes:
|
||||
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "Release notes for this version"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
2. **The CI workflow will:**
|
||||
- Run lint, format check, and type check
|
||||
- Build the project
|
||||
- Update all `package.json` versions to match the tag
|
||||
- Generate/update `CHANGELOG.md`
|
||||
- Create a release commit on `main`
|
||||
- Publish a Gitea release
|
||||
|
||||
## Version Requirements
|
||||
|
||||
- Tag must match pattern `v*` (e.g., `v1.0.0`, `v2.3.1`)
|
||||
- Tag message must not be empty (annotated tag required)
|
||||
- Tag must be pushed from `main` branch
|
||||
|
||||
# Roadmap
|
||||
|
||||
32
app/Dockerfile
Normal file
32
app/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM jimfx/nodes:latest AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json Cargo.lock Cargo.toml ./
|
||||
|
||||
COPY packages/ ./packages/
|
||||
COPY nodes/ ./nodes/
|
||||
COPY app/package.json ./app/
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
pnpm build:nodes && \
|
||||
pnpm --filter @nodarium/app... build
|
||||
|
||||
FROM nginx:alpine AS runner
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY app/docker/app.conf /etc/nginx/conf.d/app.conf
|
||||
|
||||
COPY --from=builder /app/app/build /app
|
||||
|
||||
EXPOSE 80
|
||||
10
app/docker/app.conf
Normal file
10
app/docker/app.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /app;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
}
|
||||
37
app/eslint.config.mjs
Normal file
37
app/eslint.config.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
|
||||
|
||||
export default defineConfig(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
{
|
||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||
rules: {
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -7,40 +7,51 @@
|
||||
"dev": "vite dev",
|
||||
"build": "svelte-kit sync && vite build",
|
||||
"test": "vitest",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"format": "dprint fmt -c '../.dprint.jsonc' .",
|
||||
"format:check": "dprint check -c '../.dprint.jsonc' .",
|
||||
"lint": "eslint .",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodarium/registry": "link:../packages/registry",
|
||||
"@nodarium/ui": "link:../packages/ui",
|
||||
"@nodarium/utils": "link:../packages/utils",
|
||||
"@sveltejs/kit": "^2.49.0",
|
||||
"@threlte/core": "8.3.0",
|
||||
"@threlte/extras": "9.7.0",
|
||||
"@types/three": "^0.181.0",
|
||||
"@unocss/reset": "^66.5.9",
|
||||
"@nodarium/ui": "workspace:*",
|
||||
"@nodarium/utils": "workspace:*",
|
||||
"@sveltejs/kit": "^2.50.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@threlte/core": "8.3.1",
|
||||
"@threlte/extras": "9.7.1",
|
||||
"comlink": "^4.4.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"idb": "^8.0.3",
|
||||
"jsondiffpatch": "^0.7.3",
|
||||
"three": "^0.181.2"
|
||||
"tailwindcss": "^4.1.18",
|
||||
"three": "^0.182.0",
|
||||
"wabt": "^1.0.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/tabler": "^1.2.23",
|
||||
"@nodarium/types": "link:../packages/types",
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@iconify-json/tabler": "^1.2.26",
|
||||
"@iconify/tailwind4": "^1.2.1",
|
||||
"@nodarium/types": "workspace:",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tsconfig/svelte": "^5.0.6",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@unocss/preset-icons": "^66.5.9",
|
||||
"svelte": "^5.43.14",
|
||||
"svelte-check": "^4.3.4",
|
||||
"@types/three": "^0.182.0",
|
||||
"dprint": "^0.51.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"globals": "^17.3.0",
|
||||
"svelte": "^5.46.4",
|
||||
"svelte-check": "^4.3.5",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"unocss": "^66.5.9",
|
||||
"vite": "^7.2.4",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-comlink": "^5.3.0",
|
||||
"vite-plugin-glsl": "^1.5.4",
|
||||
"vite-plugin-glsl": "^1.5.5",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vitest": "^4.0.13"
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
1576
app/pnpm-lock.yaml
generated
1576
app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
10
app/src/app.css
Normal file
10
app/src/app.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
@source "../../packages/ui/**/*.svelte";
|
||||
@plugin "@iconify/tailwind4" {
|
||||
prefix: "i";
|
||||
icon-sets: from-folder(custom, "./src/lib/icons");
|
||||
}
|
||||
|
||||
body * {
|
||||
color: var(--color-text);
|
||||
}
|
||||
14
app/src/app.d.ts
vendored
14
app/src/app.d.ts
vendored
@@ -1,13 +1,13 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
<title>Nodes</title>
|
||||
<script>
|
||||
var store = localStorage.getItem('node-settings');
|
||||
if (store) {
|
||||
try {
|
||||
var value = JSON.parse(store);
|
||||
var themes = ['dark', 'light', 'catppuccin'];
|
||||
if (themes[value.theme]) {
|
||||
document.documentElement.classList.add('theme-' + themes[value.theme]);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script defer src="https://umami.max-richter.dev/script.js" data-website-id="585c442b-0524-4874-8955-f9853b44b17e"></script>
|
||||
%sveltekit.head%
|
||||
<title>Nodes</title>
|
||||
<script>
|
||||
var store = localStorage.getItem("node-settings");
|
||||
if (store) {
|
||||
try {
|
||||
var value = JSON.parse(store);
|
||||
var themes = ["dark", "light", "catppuccin"];
|
||||
if (themes[value.theme]) {
|
||||
document.documentElement.classList.add("theme-" + themes[value.theme]);
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
app/src/lib/config.ts
Normal file
2
app/src/lib/config.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { PUBLIC_ANALYTIC_SCRIPT } from '$env/static/public';
|
||||
export const ANALYTIC_SCRIPT = PUBLIC_ANALYTIC_SCRIPT;
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { T } from "@threlte/core";
|
||||
import BackgroundVert from "./Background.vert";
|
||||
import BackgroundFrag from "./Background.frag";
|
||||
import { colors } from "../graph/colors.svelte";
|
||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import { T } from '@threlte/core';
|
||||
import { colors } from '../graph/colors.svelte';
|
||||
import BackgroundFrag from './Background.frag';
|
||||
import BackgroundVert from './Background.vert';
|
||||
|
||||
type Props = {
|
||||
minZoom: number;
|
||||
@@ -18,7 +18,7 @@
|
||||
maxZoom = 150,
|
||||
cameraPosition = [0, 1, 0],
|
||||
width = globalThis?.innerWidth || 100,
|
||||
height = globalThis?.innerHeight || 100,
|
||||
height = globalThis?.innerHeight || 100
|
||||
}: Props = $props();
|
||||
|
||||
let bw = $derived(width / cameraPosition[2]);
|
||||
@@ -38,25 +38,25 @@
|
||||
fragmentShader={BackgroundFrag}
|
||||
uniforms={{
|
||||
camPos: {
|
||||
value: [0, 1, 0],
|
||||
value: [0, 1, 0]
|
||||
},
|
||||
backgroundColor: {
|
||||
value: colors["layer-0"],
|
||||
value: colors['layer-0']
|
||||
},
|
||||
lineColor: {
|
||||
value: colors["outline"],
|
||||
value: colors['outline']
|
||||
},
|
||||
zoomLimits: {
|
||||
value: [2, 50],
|
||||
value: [2, 50]
|
||||
},
|
||||
dimensions: {
|
||||
value: [100, 100],
|
||||
},
|
||||
value: [100, 100]
|
||||
}
|
||||
}}
|
||||
uniforms.camPos.value={cameraPosition}
|
||||
uniforms.backgroundColor.value={appSettings.value.theme &&
|
||||
colors["layer-0"]}
|
||||
uniforms.lineColor.value={appSettings.value.theme && colors["outline"]}
|
||||
uniforms.backgroundColor.value={appSettings.value.theme
|
||||
&& colors['layer-0']}
|
||||
uniforms.lineColor.value={appSettings.value.theme && colors['outline']}
|
||||
uniforms.zoomLimits.value={[minZoom, maxZoom]}
|
||||
uniforms.dimensions.value={[width, height]}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { HTML } from "@threlte/extras";
|
||||
import { onMount } from "svelte";
|
||||
import type { Node, NodeType } from "@nodarium/types";
|
||||
import { getGraphManager, getGraphState } from "../graph/state.svelte";
|
||||
import type { NodeId, NodeInstance } from '@nodarium/types';
|
||||
import { HTML } from '@threlte/extras';
|
||||
import { onMount } from 'svelte';
|
||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||
|
||||
type Props = {
|
||||
onnode: (n: Node) => void;
|
||||
onnode: (n: NodeInstance) => void;
|
||||
};
|
||||
|
||||
const { onnode }: Props = $props();
|
||||
@@ -14,18 +14,19 @@
|
||||
const graphState = getGraphState();
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let wrapper: HTMLDivElement;
|
||||
let value = $state<string>();
|
||||
let activeNodeId = $state<NodeType>();
|
||||
let activeNodeId = $state<NodeId>();
|
||||
|
||||
const allNodes = graphState.activeSocket
|
||||
? graph.getPossibleNodes(graphState.activeSocket)
|
||||
: graph.getNodeDefinitions();
|
||||
|
||||
function filterNodes() {
|
||||
return allNodes.filter((node) => node.id.includes(value ?? ""));
|
||||
return allNodes.filter((node) => node.id.includes(value ?? ''));
|
||||
}
|
||||
|
||||
const nodes = $derived(value === "" ? allNodes : filterNodes());
|
||||
const nodes = $derived(value === '' ? allNodes : filterNodes());
|
||||
$effect(() => {
|
||||
if (nodes) {
|
||||
if (activeNodeId === undefined) {
|
||||
@@ -39,37 +40,38 @@
|
||||
}
|
||||
});
|
||||
|
||||
function handleNodeCreation(nodeType: Node["type"]) {
|
||||
function handleNodeCreation(nodeType: NodeInstance['type']) {
|
||||
if (!graphState.addMenuPosition) return;
|
||||
onnode?.({
|
||||
id: -1,
|
||||
type: nodeType,
|
||||
position: [...graphState.addMenuPosition],
|
||||
props: {},
|
||||
state: {}
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
if (event.key === "Escape") {
|
||||
if (event.key === 'Escape') {
|
||||
graphState.addMenuPosition = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
if (event.key === 'ArrowDown') {
|
||||
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
||||
activeNodeId = nodes[(index + 1) % nodes.length].id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
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 (event.key === 'Enter') {
|
||||
if (activeNodeId && graphState.addMenuPosition) {
|
||||
handleNodeCreation(activeNodeId);
|
||||
}
|
||||
@@ -80,6 +82,16 @@
|
||||
onMount(() => {
|
||||
input.disabled = false;
|
||||
setTimeout(() => input.focus(), 50);
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const deltaY = rect.bottom - window.innerHeight;
|
||||
const deltaX = rect.right - window.innerWidth;
|
||||
if (deltaY > 0) {
|
||||
wrapper.style.marginTop = `-${deltaY + 30}px`;
|
||||
}
|
||||
if (deltaX > 0) {
|
||||
wrapper.style.marginLeft = `-${deltaX + 30}px`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -88,7 +100,7 @@
|
||||
position.z={graphState.addMenuPosition?.[1]}
|
||||
transform={false}
|
||||
>
|
||||
<div class="add-menu-wrapper">
|
||||
<div class="add-menu-wrapper" bind:this={wrapper}>
|
||||
<div class="header">
|
||||
<input
|
||||
id="add-menu"
|
||||
@@ -104,14 +116,14 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{#each nodes as node}
|
||||
{#each nodes as node (node.id)}
|
||||
<div
|
||||
class="result"
|
||||
role="treeitem"
|
||||
tabindex="0"
|
||||
aria-selected={node.id === activeNodeId}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
if (event.key === 'Enter') {
|
||||
handleNodeCreation(node.id);
|
||||
}
|
||||
}}
|
||||
@@ -124,7 +136,7 @@
|
||||
activeNodeId = node.id;
|
||||
}}
|
||||
>
|
||||
{node.id.split("/").at(-1)}
|
||||
{node.id.split('/').at(-1)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -137,7 +149,7 @@
|
||||
}
|
||||
|
||||
input {
|
||||
background: var(--layer-0);
|
||||
background: var(--color-layer-0);
|
||||
font-family: var(--font-family);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
@@ -156,28 +168,30 @@
|
||||
|
||||
.add-menu-wrapper {
|
||||
position: absolute;
|
||||
background: var(--layer-1);
|
||||
background: var(--color-layer-1);
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
border: solid 2px var(--layer-2);
|
||||
border: solid 2px var(--color-layer-2);
|
||||
width: 150px;
|
||||
}
|
||||
.content {
|
||||
min-height: none;
|
||||
width: 100%;
|
||||
color: var(--text-color);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: 1em 0.9em;
|
||||
border-bottom: solid 1px var(--layer-2);
|
||||
border-bottom: solid 1px var(--color-layer-2);
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result[aria-selected="true"] {
|
||||
background: var(--layer-2);
|
||||
background: var(--color-layer-2);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { HTML } from "@threlte/extras";
|
||||
import { HTML } from '@threlte/extras';
|
||||
|
||||
type Props = {
|
||||
p1: { x: number; y: number };
|
||||
@@ -10,7 +10,7 @@
|
||||
const {
|
||||
p1 = { x: 0, y: 0 },
|
||||
p2 = { x: 0, y: 0 },
|
||||
cameraPosition = [0, 1, 0],
|
||||
cameraPosition = [0, 1, 0]
|
||||
}: Props = $props();
|
||||
|
||||
const width = $derived(Math.abs(p1.x - p2.x) * cameraPosition[2]);
|
||||
@@ -24,14 +24,15 @@
|
||||
<div
|
||||
class="box-selection"
|
||||
style={`width: ${width}px; height: ${height}px;`}
|
||||
></div>
|
||||
>
|
||||
</div>
|
||||
</HTML>
|
||||
|
||||
<style>
|
||||
.box-selection {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
border: solid 2px var(--outline);
|
||||
border: solid 2px var(--color-outline);
|
||||
border-style: dashed;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { T } from "@threlte/core";
|
||||
import { type OrthographicCamera } from "three";
|
||||
import { T } from '@threlte/core';
|
||||
import { type OrthographicCamera } from 'three';
|
||||
type Props = {
|
||||
camera: OrthographicCamera;
|
||||
position: [number, number, number];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { NodeDefinition, NodeRegistry } from "@nodarium/types";
|
||||
import { onMount } from "svelte";
|
||||
import type { NodeDefinition, NodeRegistry } from '@nodarium/types';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let mx = $state(0);
|
||||
let my = $state(0);
|
||||
@@ -20,15 +20,15 @@
|
||||
my = ev.clientY;
|
||||
if (!target) return;
|
||||
|
||||
const closest = target?.closest?.("[data-node-type]");
|
||||
const closest = target?.closest?.('[data-node-type]');
|
||||
|
||||
if (!closest) {
|
||||
node = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let nodeType = closest.getAttribute("data-node-type");
|
||||
let nodeInput = closest.getAttribute("data-node-input");
|
||||
let nodeType = closest.getAttribute('data-node-type');
|
||||
let nodeInput = closest.getAttribute('data-node-input');
|
||||
|
||||
if (!nodeType) {
|
||||
node = undefined;
|
||||
@@ -40,9 +40,9 @@
|
||||
|
||||
onMount(() => {
|
||||
const style = wrapper.parentElement?.style;
|
||||
style?.setProperty("cursor", "help");
|
||||
style?.setProperty('cursor', 'help');
|
||||
return () => {
|
||||
style?.removeProperty("cursor");
|
||||
style?.removeProperty('cursor');
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -53,12 +53,12 @@
|
||||
class="help-wrapper p-4"
|
||||
class:visible={node}
|
||||
bind:clientWidth={width}
|
||||
style="--my:{my}px; --mx:{Math.min(mx, window.innerWidth - width - 20)}px;"
|
||||
style="--my: {my}px; --mx: {Math.min(mx, window.innerWidth - width - 20)}px"
|
||||
bind:this={wrapper}
|
||||
>
|
||||
<p class="m-0 text-light opacity-40 flex items-center gap-3 mb-4">
|
||||
<span class="i-tabler-help block w-4 h-4"></span>
|
||||
{node?.id.split("/").at(-1) || "Help"}
|
||||
{node?.id.split('/').at(-1) || 'Help'}
|
||||
{#if input}
|
||||
<span>> {input}</span>
|
||||
{/if}
|
||||
@@ -77,7 +77,7 @@
|
||||
{#if !input}
|
||||
<div>
|
||||
<span class="i-tabler-arrow-right opacity-30">-></span>
|
||||
{node?.outputs?.map((o) => o).join(", ") ?? "nothing"}
|
||||
{node?.outputs?.map((o) => o).join(', ') ?? 'nothing'}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -88,12 +88,12 @@
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
transform: translate(var(--mx), var(--my));
|
||||
background: var(--layer-1);
|
||||
background: var(--color-layer-1);
|
||||
border-radius: 5px;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
max-width: 250px;
|
||||
border: 1px solid var(--outline);
|
||||
border: 1px solid var(--color-outline);
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||
import { points, lines } from './store.js';
|
||||
import { T } from '@threlte/core';
|
||||
import type { Box } from '@nodarium/types';
|
||||
import { T } from '@threlte/core';
|
||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||
import { Color, Vector3 } from 'three';
|
||||
import { lines, points, rects } from './store.js';
|
||||
|
||||
type Line = {
|
||||
points: Vector3[];
|
||||
color?: Color;
|
||||
};
|
||||
|
||||
function getEachKey(value: Vector3 | Box | Line): string {
|
||||
if ('x' in value) {
|
||||
return [value.x, value.y, value.z].join('-');
|
||||
}
|
||||
if ('minX' in value) {
|
||||
return [value.maxX, value.minX, value.maxY, value.minY].join('-');
|
||||
}
|
||||
|
||||
if ('points' in value) {
|
||||
return getEachKey(value.points[Math.floor(value.points.length / 2)]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each $points as point}
|
||||
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}>
|
||||
<T.CircleGeometry args={[0.2, 32]} />
|
||||
<T.MeshBasicMaterial color="red" />
|
||||
</T.Mesh>
|
||||
{#each $points as point (getEachKey(point))}
|
||||
<T.Mesh
|
||||
position.x={point.x}
|
||||
position.y={point.y}
|
||||
position.z={point.z}
|
||||
rotation.x={-Math.PI / 2}
|
||||
>
|
||||
<T.CircleGeometry args={[0.2, 32]} />
|
||||
<T.MeshBasicMaterial color="red" />
|
||||
</T.Mesh>
|
||||
{/each}
|
||||
|
||||
{#each $lines as line}
|
||||
<T.Mesh>
|
||||
<MeshLineGeometry points={line} />
|
||||
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
||||
</T.Mesh>
|
||||
{#each $rects as rect, i (getEachKey(rect))}
|
||||
<T.Mesh
|
||||
position.x={(rect.minX + rect.maxX) / 2}
|
||||
position.y={0}
|
||||
position.z={(rect.minY + rect.maxY) / 2}
|
||||
rotation.x={-Math.PI / 2}
|
||||
>
|
||||
<T.PlaneGeometry args={[rect.maxX - rect.minX, rect.maxY - rect.minY]} />
|
||||
<T.MeshBasicMaterial
|
||||
color={new Color().setHSL((i * 1.77) % 1, 1, 0.5)}
|
||||
opacity={0.9}
|
||||
/>
|
||||
</T.Mesh>
|
||||
{/each}
|
||||
|
||||
{#each $lines as line (getEachKey(line))}
|
||||
<T.Mesh position.y={1}>
|
||||
<MeshLineGeometry points={line.points} />
|
||||
<MeshLineMaterial
|
||||
color={line.color || 'red'}
|
||||
linewidth={1}
|
||||
attenuate={false}
|
||||
/>
|
||||
</T.Mesh>
|
||||
{/each}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Vector3 } from "three/src/math/Vector3.js";
|
||||
import { lines, points } from "./store";
|
||||
import type { Box } from '@nodarium/types';
|
||||
import type { Color } from 'three';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
import Component from './Debug.svelte';
|
||||
import { lines, points, rects } from './store';
|
||||
|
||||
export function debugPosition(x: number, y: number) {
|
||||
points.update((p) => {
|
||||
@@ -8,18 +11,27 @@ export function debugPosition(x: number, y: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function debugRect(rect: Box) {
|
||||
rects.update((r) => {
|
||||
r.push(rect);
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
points.set([]);
|
||||
lines.set([]);
|
||||
rects.set([]);
|
||||
}
|
||||
|
||||
export function debugLine(line: Vector3[]) {
|
||||
export function debugLine(points: Vector3[], color?: Color) {
|
||||
lines.update((l) => {
|
||||
l.push(line);
|
||||
l.push({ points, color });
|
||||
return l;
|
||||
});
|
||||
}
|
||||
|
||||
import Component from "./Debug.svelte";
|
||||
|
||||
export default Component
|
||||
export default Component;
|
||||
export function clearLines() {
|
||||
lines.set([]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { Vector3 } from "three/src/math/Vector3.js";
|
||||
import type { Box } from '@nodarium/types';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Color } from 'three';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
export const points = writable<Vector3[]>([]);
|
||||
|
||||
export const lines = writable<Vector3[][]>([]);
|
||||
export const rects = writable<Box[]>([]);
|
||||
export const lines = writable<{ points: Vector3[]; color?: Color }[]>([]);
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script module lang="ts">
|
||||
import { colors } from "../graph/colors.svelte";
|
||||
import { colors } from '../graph/colors.svelte';
|
||||
|
||||
const circleMaterial = new MeshBasicMaterial({
|
||||
color: colors.edge.clone(),
|
||||
toneMapped: false,
|
||||
toneMapped: false
|
||||
});
|
||||
|
||||
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
appSettings.value.theme;
|
||||
if (appSettings.value.theme === undefined) {
|
||||
return;
|
||||
}
|
||||
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
|
||||
lineColor = colors.edge.clone().convertSRGBToLinear();
|
||||
});
|
||||
@@ -20,17 +22,21 @@
|
||||
new Vector2(0, 0),
|
||||
new Vector2(0, 0),
|
||||
new Vector2(0, 0),
|
||||
new Vector2(0, 0),
|
||||
new Vector2(0, 0)
|
||||
);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { T } from "@threlte/core";
|
||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||
import { Mesh, MeshBasicMaterial, Vector3 } from "three";
|
||||
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
|
||||
import { Vector2 } from "three/src/math/Vector2.js";
|
||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import { T } from '@threlte/core';
|
||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { MeshBasicMaterial, Vector3 } from 'three';
|
||||
import { CubicBezierCurve } from 'three/src/extras/curves/CubicBezierCurve.js';
|
||||
import { Vector2 } from 'three/src/math/Vector2.js';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
type Props = {
|
||||
x1: number;
|
||||
@@ -38,27 +44,28 @@
|
||||
x2: number;
|
||||
y2: number;
|
||||
z: number;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const { x1, y1, x2, y2, z }: Props = $props();
|
||||
const { x1, y1, x2, y2, z, id }: Props = $props();
|
||||
|
||||
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
|
||||
|
||||
let points = $state<Vector3[]>([]);
|
||||
|
||||
let lastId: string | null = null;
|
||||
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
|
||||
|
||||
function update() {
|
||||
const new_x = x2 - x1;
|
||||
const new_y = y2 - y1;
|
||||
const curveId = `${x1}-${y1}-${x2}-${y2}`;
|
||||
if (lastId === curveId) {
|
||||
return;
|
||||
}
|
||||
lastId = curveId;
|
||||
|
||||
const length = Math.floor(
|
||||
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
|
||||
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4
|
||||
);
|
||||
|
||||
const samples = Math.max(length * 16, 10);
|
||||
@@ -72,6 +79,15 @@
|
||||
.getPoints(samples)
|
||||
.map((p) => new Vector3(p.x, 0, p.y))
|
||||
.flat();
|
||||
|
||||
if (id) {
|
||||
graphState.setEdgeGeometry(
|
||||
id,
|
||||
x1,
|
||||
y1,
|
||||
$state.snapshot(points) as unknown as Vector3[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -79,6 +95,10 @@
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (id) graphState.removeEdgeGeometry(id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<T.Mesh
|
||||
@@ -101,6 +121,18 @@
|
||||
<T.CircleGeometry args={[0.5, 16]} />
|
||||
</T.Mesh>
|
||||
|
||||
{#if graphState.hoveredEdgeId === id}
|
||||
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
||||
<MeshLineGeometry {points} />
|
||||
<MeshLineMaterial
|
||||
width={thickness * 5}
|
||||
color={lineColor}
|
||||
opacity={0.5}
|
||||
transparent
|
||||
/>
|
||||
</T.Mesh>
|
||||
{/if}
|
||||
|
||||
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
||||
<MeshLineGeometry {points} />
|
||||
<MeshLineMaterial width={thickness} color={lineColor} />
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
export const setXYZXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
|
||||
array[location + 0] = x
|
||||
array[location + 1] = y
|
||||
array[location + 2] = z
|
||||
array[location + 0] = x;
|
||||
array[location + 1] = y;
|
||||
array[location + 2] = z;
|
||||
|
||||
array[location + 3] = x
|
||||
array[location + 4] = y
|
||||
array[location + 5] = z
|
||||
}
|
||||
array[location + 3] = x;
|
||||
array[location + 4] = y;
|
||||
array[location + 5] = z;
|
||||
};
|
||||
|
||||
export const setXY = (array: number[], location: number, x: number, y: number) => {
|
||||
array[location + 0] = x
|
||||
array[location + 1] = y
|
||||
}
|
||||
array[location + 0] = x;
|
||||
array[location + 1] = y;
|
||||
};
|
||||
|
||||
export const setXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
|
||||
array[location + 0] = x
|
||||
array[location + 1] = y
|
||||
array[location + 2] = z
|
||||
}
|
||||
array[location + 0] = x;
|
||||
array[location + 1] = y;
|
||||
array[location + 2] = z;
|
||||
};
|
||||
|
||||
export const setXYZW = (
|
||||
array: number[],
|
||||
@@ -27,8 +27,8 @@ export const setXYZW = (
|
||||
z: number,
|
||||
w: number
|
||||
) => {
|
||||
array[location + 0] = x
|
||||
array[location + 1] = y
|
||||
array[location + 2] = z
|
||||
array[location + 3] = w
|
||||
}
|
||||
array[location + 0] = x;
|
||||
array[location + 1] = y;
|
||||
array[location + 2] = z;
|
||||
array[location + 3] = w;
|
||||
};
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import throttle from '$lib/helpers/throttle';
|
||||
import { RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||
import type {
|
||||
Edge,
|
||||
Graph,
|
||||
Node,
|
||||
NodeDefinition,
|
||||
NodeId,
|
||||
NodeInput,
|
||||
NodeInstance,
|
||||
NodeRegistry,
|
||||
NodeType,
|
||||
Socket,
|
||||
} from "@nodarium/types";
|
||||
import { fastHashString } from "@nodarium/utils";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import EventEmitter from "./helpers/EventEmitter";
|
||||
import { createLogger } from "@nodarium/utils";
|
||||
import throttle from "$lib/helpers/throttle";
|
||||
import { HistoryManager } from "./history-manager";
|
||||
Socket
|
||||
} from '@nodarium/types';
|
||||
import { fastHashString } from '@nodarium/utils';
|
||||
import { createLogger } from '@nodarium/utils';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import EventEmitter from './helpers/EventEmitter';
|
||||
import { HistoryManager } from './history-manager';
|
||||
|
||||
const logger = createLogger("graph-manager");
|
||||
const logger = createLogger('graph-manager');
|
||||
logger.mute();
|
||||
|
||||
const clone =
|
||||
"structuredClone" in self
|
||||
? self.structuredClone
|
||||
: (args: any) => JSON.parse(JSON.stringify(args));
|
||||
const remoteRegistry = new RemoteNodeRegistry('');
|
||||
|
||||
const clone = 'structuredClone' in self
|
||||
? self.structuredClone
|
||||
: (args: unknown) => JSON.parse(JSON.stringify(args));
|
||||
|
||||
function areSocketsCompatible(
|
||||
output: string | undefined,
|
||||
inputs: string | (string | undefined)[] | undefined,
|
||||
inputs: string | (string | undefined)[] | undefined
|
||||
) {
|
||||
if (Array.isArray(inputs) && output) {
|
||||
return inputs.includes(output);
|
||||
@@ -34,41 +36,40 @@ function areSocketsCompatible(
|
||||
}
|
||||
|
||||
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
||||
|
||||
if (firstEdge[0].id !== secondEdge[0].id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstEdge[1] !== secondEdge[1]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstEdge[2].id !== secondEdge[2].id) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstEdge[3] !== secondEdge[3]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
export class GraphManager extends EventEmitter<{
|
||||
save: Graph;
|
||||
result: any;
|
||||
result: unknown;
|
||||
settings: {
|
||||
types: Record<string, NodeInput>;
|
||||
values: Record<string, unknown>;
|
||||
};
|
||||
}> {
|
||||
status = $state<"loading" | "idle" | "error">();
|
||||
status = $state<'loading' | 'idle' | 'error'>();
|
||||
loaded = false;
|
||||
|
||||
graph: Graph = { id: 0, nodes: [], edges: [] };
|
||||
id = $state(0);
|
||||
|
||||
nodes = new SvelteMap<number, Node>();
|
||||
nodes = new SvelteMap<number, NodeInstance>();
|
||||
|
||||
edges = $state<Edge[]>([]);
|
||||
|
||||
@@ -78,7 +79,7 @@ export class GraphManager extends EventEmitter<{
|
||||
currentUndoGroup: number | null = null;
|
||||
|
||||
inputSockets = $derived.by(() => {
|
||||
const s = new Set<string>();
|
||||
const s = new SvelteSet<string>();
|
||||
for (const edge of this.edges) {
|
||||
s.add(`${edge[2].id}-${edge[3]}`);
|
||||
}
|
||||
@@ -88,7 +89,7 @@ export class GraphManager extends EventEmitter<{
|
||||
history: HistoryManager = new HistoryManager();
|
||||
execute = throttle(() => {
|
||||
if (this.loaded === false) return;
|
||||
this.emit("result", this.serialize());
|
||||
this.emit('result', this.serialize());
|
||||
}, 10);
|
||||
|
||||
constructor(public registry: NodeRegistry) {
|
||||
@@ -100,27 +101,28 @@ export class GraphManager extends EventEmitter<{
|
||||
id: node.id,
|
||||
position: [...node.position],
|
||||
type: node.type,
|
||||
props: node.props,
|
||||
})) as Node[];
|
||||
props: node.props
|
||||
})) as NodeInstance[];
|
||||
const edges = this.edges.map((edge) => [
|
||||
edge[0].id,
|
||||
edge[1],
|
||||
edge[2].id,
|
||||
edge[3],
|
||||
]) as Graph["edges"];
|
||||
edge[3]
|
||||
]) as Graph['edges'];
|
||||
const serialized = {
|
||||
id: this.graph.id,
|
||||
settings: $state.snapshot(this.settings),
|
||||
meta: $state.snapshot(this.graph.meta),
|
||||
nodes,
|
||||
edges,
|
||||
edges
|
||||
};
|
||||
logger.log("serializing graph", serialized);
|
||||
logger.log('serializing graph', serialized);
|
||||
return clone($state.snapshot(serialized));
|
||||
}
|
||||
|
||||
private lastSettingsHash = 0;
|
||||
setSettings(settings: Record<string, unknown>) {
|
||||
let hash = fastHashString(JSON.stringify(settings));
|
||||
const hash = fastHashString(JSON.stringify(settings));
|
||||
if (hash === this.lastSettingsHash) return;
|
||||
this.lastSettingsHash = hash;
|
||||
|
||||
@@ -133,8 +135,8 @@ export class GraphManager extends EventEmitter<{
|
||||
return this.registry.getAllNodes();
|
||||
}
|
||||
|
||||
getLinkedNodes(node: Node) {
|
||||
const nodes = new Set<Node>();
|
||||
getLinkedNodes(node: NodeInstance) {
|
||||
const nodes = new SvelteSet<NodeInstance>();
|
||||
const stack = [node];
|
||||
while (stack.length) {
|
||||
const n = stack.pop();
|
||||
@@ -148,21 +150,112 @@ export class GraphManager extends EventEmitter<{
|
||||
return [...nodes.values()];
|
||||
}
|
||||
|
||||
getEdgesBetweenNodes(nodes: Node[]): [number, number, number, string][] {
|
||||
getEdgeId(e: Edge) {
|
||||
return `${e[0].id}-${e[1]}-${e[2].id}-${e[3]}`;
|
||||
}
|
||||
|
||||
getEdgeById(id: string): Edge | undefined {
|
||||
return this.edges.find((e) => this.getEdgeId(e) === id);
|
||||
}
|
||||
|
||||
dropNodeOnEdge(nodeId: number, edge: Edge) {
|
||||
const draggedNode = this.getNode(nodeId);
|
||||
if (!draggedNode || !draggedNode.state?.type) return;
|
||||
|
||||
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
||||
|
||||
const draggedInputs = Object.entries(draggedNode.state.type.inputs ?? {});
|
||||
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
||||
|
||||
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
|
||||
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
||||
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
||||
|
||||
const bestInputEntry = draggedInputs.find(([, input]) => {
|
||||
const accepted = [input.type, ...(input.accepts || [])];
|
||||
return areSocketsCompatible(edgeOutputSocketType, accepted);
|
||||
});
|
||||
|
||||
const bestOutputIdx = draggedOutputs.findIndex(outputType =>
|
||||
areSocketsCompatible(outputType, targetAcceptedTypes)
|
||||
);
|
||||
|
||||
if (!bestInputEntry || bestOutputIdx === -1) {
|
||||
logger.error('Could not find compatible sockets for drop');
|
||||
return;
|
||||
}
|
||||
|
||||
this.startUndoGroup();
|
||||
|
||||
this.removeEdge(edge, { applyDeletion: false });
|
||||
|
||||
this.createEdge(fromNode, fromSocketIdx, draggedNode, bestInputEntry[0], {
|
||||
applyUpdate: false
|
||||
});
|
||||
|
||||
this.createEdge(draggedNode, bestOutputIdx, toNode, toSocketKey, {
|
||||
applyUpdate: false
|
||||
});
|
||||
|
||||
this.saveUndoGroup();
|
||||
this.execute();
|
||||
}
|
||||
|
||||
getPossibleDropOnEdges(nodeId: number): Edge[] {
|
||||
const draggedNode = this.getNode(nodeId);
|
||||
if (!draggedNode || !draggedNode.state?.type) return [];
|
||||
|
||||
const draggedInputs = Object.values(draggedNode.state.type.inputs ?? {});
|
||||
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
||||
|
||||
// Optimization: Pre-calculate parents to avoid cycles
|
||||
const parentIds = new SvelteSet(this.getParentsOfNode(draggedNode).map(n => n.id));
|
||||
|
||||
return this.edges.filter((edge) => {
|
||||
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
||||
|
||||
// 1. Prevent cycles: If the target node is already a parent, we can't drop here
|
||||
if (parentIds.has(toNode.id)) return false;
|
||||
|
||||
// 2. Prevent self-dropping: Don't drop on edges already connected to this node
|
||||
if (fromNode.id === nodeId || toNode.id === nodeId) return false;
|
||||
|
||||
// 3. Check if edge.source can plug into ANY draggedNode.input
|
||||
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
|
||||
const canPlugIntoDragged = draggedInputs.some(input => {
|
||||
const acceptedTypes = [input.type, ...(input.accepts || [])];
|
||||
return areSocketsCompatible(edgeOutputSocketType, acceptedTypes);
|
||||
});
|
||||
|
||||
if (!canPlugIntoDragged) return false;
|
||||
|
||||
// 4. Check if ANY draggedNode.output can plug into edge.target
|
||||
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
||||
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
||||
|
||||
const draggedCanPlugIntoTarget = draggedOutputs.some(outputType =>
|
||||
areSocketsCompatible(outputType, targetAcceptedTypes)
|
||||
);
|
||||
|
||||
return draggedCanPlugIntoTarget;
|
||||
});
|
||||
}
|
||||
|
||||
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
|
||||
const edges = [];
|
||||
for (const node of nodes) {
|
||||
const children = node.tmp?.children || [];
|
||||
const children = node.state?.children || [];
|
||||
for (const child of children) {
|
||||
if (nodes.includes(child)) {
|
||||
const edge = this.edges.find(
|
||||
(e) => e[0].id === node.id && e[2].id === child.id,
|
||||
(e) => e[0].id === node.id && e[2].id === child.id
|
||||
);
|
||||
if (edge) {
|
||||
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
string,
|
||||
string
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -173,35 +266,32 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
private _init(graph: Graph) {
|
||||
const nodes = new Map(
|
||||
graph.nodes.map((node: Node) => {
|
||||
const nodes = new SvelteMap(
|
||||
graph.nodes.map((node) => {
|
||||
const nodeType = this.registry.getNode(node.type);
|
||||
const n = node as NodeInstance;
|
||||
if (nodeType) {
|
||||
node.tmp = {
|
||||
type: nodeType,
|
||||
n.state = {
|
||||
type: nodeType
|
||||
};
|
||||
}
|
||||
return [node.id, node];
|
||||
}),
|
||||
return [node.id, n];
|
||||
})
|
||||
);
|
||||
|
||||
const edges = graph.edges.map((edge) => {
|
||||
this.edges = graph.edges.map((edge) => {
|
||||
const from = nodes.get(edge[0]);
|
||||
const to = nodes.get(edge[2]);
|
||||
if (!from || !to) {
|
||||
throw new Error("Edge references non-existing node");
|
||||
throw new Error('Edge references non-existing node');
|
||||
}
|
||||
from.tmp = from.tmp || {};
|
||||
from.tmp.children = from.tmp.children || [];
|
||||
from.tmp.children.push(to);
|
||||
to.tmp = to.tmp || {};
|
||||
to.tmp.parents = to.tmp.parents || [];
|
||||
to.tmp.parents.push(from);
|
||||
from.state.children = from.state.children || [];
|
||||
from.state.children.push(to);
|
||||
to.state.parents = to.state.parents || [];
|
||||
to.state.parents.push(from);
|
||||
return [from, edge[1], to, edge[3]] as Edge;
|
||||
});
|
||||
|
||||
this.edges = [...edges];
|
||||
|
||||
this.nodes.clear();
|
||||
for (const [id, node] of nodes) {
|
||||
this.nodes.set(id, node);
|
||||
@@ -215,25 +305,42 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
this.loaded = false;
|
||||
this.graph = graph;
|
||||
this.status = "loading";
|
||||
this.status = 'loading';
|
||||
this.id = graph.id;
|
||||
|
||||
logger.info("loading graph", $state.snapshot(graph));
|
||||
logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id });
|
||||
|
||||
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
|
||||
const nodeIds = Array.from(new SvelteSet([...graph.nodes.map((n) => n.type)]));
|
||||
await this.registry.load(nodeIds);
|
||||
|
||||
logger.info("loaded node types", this.registry.getAllNodes());
|
||||
// Fetch all nodes from all collections of the loaded nodes
|
||||
const allCollections = new SvelteSet<`${string}/${string}`>();
|
||||
for (const id of nodeIds) {
|
||||
const [user, collection] = id.split('/');
|
||||
allCollections.add(`${user}/${collection}`);
|
||||
}
|
||||
for (const collection of allCollections) {
|
||||
remoteRegistry
|
||||
.fetchCollection(collection)
|
||||
.then((collection: { nodes: { id: NodeId }[] }) => {
|
||||
const ids = collection.nodes.map((n) => n.id);
|
||||
return this.registry.load(ids);
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('loaded node types', this.registry.getAllNodes());
|
||||
|
||||
for (const node of this.graph.nodes) {
|
||||
const nodeType = this.registry.getNode(node.type);
|
||||
if (!nodeType) {
|
||||
logger.error(`Node type not found: ${node.type}`);
|
||||
this.status = "error";
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
node.tmp = node.tmp || {};
|
||||
node.tmp.type = nodeType;
|
||||
// Turn into runtime node
|
||||
const n = node as NodeInstance;
|
||||
n.state = {};
|
||||
n.state.type = nodeType;
|
||||
}
|
||||
|
||||
// load settings
|
||||
@@ -247,16 +354,16 @@ export class GraphManager extends EventEmitter<{
|
||||
for (const type of types) {
|
||||
if (type.inputs) {
|
||||
for (const key in type.inputs) {
|
||||
let settingId = type.inputs[key].setting;
|
||||
const settingId = type.inputs[key].setting;
|
||||
if (settingId) {
|
||||
settingTypes[settingId] = {
|
||||
__node_type: type.id,
|
||||
__node_input: key,
|
||||
...type.inputs[key],
|
||||
...type.inputs[key]
|
||||
};
|
||||
if (
|
||||
settingValues[settingId] === undefined &&
|
||||
"value" in type.inputs[key]
|
||||
settingValues[settingId] === undefined
|
||||
&& 'value' in type.inputs[key]
|
||||
) {
|
||||
settingValues[settingId] = type.inputs[key].value;
|
||||
}
|
||||
@@ -266,14 +373,14 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
this.settings = settingValues;
|
||||
this.emit("settings", { types: settingTypes, values: settingValues });
|
||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||
|
||||
this.history.reset();
|
||||
this._init(this.graph);
|
||||
|
||||
this.save();
|
||||
|
||||
this.status = "idle";
|
||||
this.status = 'idle';
|
||||
|
||||
this.loaded = true;
|
||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||
@@ -292,7 +399,7 @@ export class GraphManager extends EventEmitter<{
|
||||
return this.registry.getNode(id);
|
||||
}
|
||||
|
||||
async loadNodeType(id: NodeType) {
|
||||
async loadNodeType(id: NodeId) {
|
||||
await this.registry.load([id]);
|
||||
const nodeType = this.registry.getNode(id);
|
||||
|
||||
@@ -302,13 +409,13 @@ export class GraphManager extends EventEmitter<{
|
||||
const settingValues = this.settings;
|
||||
if (nodeType.inputs) {
|
||||
for (const key in nodeType.inputs) {
|
||||
let settingId = nodeType.inputs[key].setting;
|
||||
const settingId = nodeType.inputs[key].setting;
|
||||
if (settingId) {
|
||||
settingTypes[settingId] = nodeType.inputs[key];
|
||||
if (
|
||||
settingValues &&
|
||||
settingValues?.[settingId] === undefined &&
|
||||
"value" in nodeType.inputs[key]
|
||||
settingValues
|
||||
&& settingValues?.[settingId] === undefined
|
||||
&& 'value' in nodeType.inputs[key]
|
||||
) {
|
||||
settingValues[settingId] = nodeType.inputs[key].value;
|
||||
}
|
||||
@@ -318,22 +425,22 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
this.settings = settingValues;
|
||||
this.settingTypes = settingTypes;
|
||||
this.emit("settings", { types: settingTypes, values: settingValues });
|
||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||
}
|
||||
|
||||
getChildren(node: Node) {
|
||||
getChildren(node: NodeInstance) {
|
||||
const children = [];
|
||||
const stack = node.tmp?.children?.slice(0);
|
||||
const stack = node.state?.children?.slice(0);
|
||||
while (stack?.length) {
|
||||
const child = stack.pop();
|
||||
if (!child) continue;
|
||||
children.push(child);
|
||||
stack.push(...(child.tmp?.children || []));
|
||||
stack.push(...(child.state?.children || []));
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
getNodesBetween(from: Node, to: Node): Node[] | undefined {
|
||||
getNodesBetween(from: NodeInstance, to: NodeInstance): NodeInstance[] | undefined {
|
||||
// < - - - - from
|
||||
const toParents = this.getParentsOfNode(to);
|
||||
// < - - - - from - - - - to
|
||||
@@ -350,7 +457,7 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
}
|
||||
|
||||
removeNode(node: Node, { restoreEdges = false } = {}) {
|
||||
removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
|
||||
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
|
||||
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
|
||||
for (const edge of [...edgesToNode, ...edgesFromNode]) {
|
||||
@@ -363,11 +470,11 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
for (const [to, toSocket] of inputSockets) {
|
||||
for (const [from, fromSocket] of outputSockets) {
|
||||
const outputType = from.tmp?.type?.outputs?.[fromSocket];
|
||||
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type;
|
||||
const outputType = from.state?.type?.outputs?.[fromSocket];
|
||||
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
|
||||
if (outputType === inputType) {
|
||||
this.createEdge(from, fromSocket, to, toSocket, {
|
||||
applyUpdate: false,
|
||||
applyUpdate: false
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -380,9 +487,9 @@ export class GraphManager extends EventEmitter<{
|
||||
this.save();
|
||||
}
|
||||
|
||||
smartConnect(from: Node, to: Node): Edge | undefined {
|
||||
const inputs = Object.entries(to.tmp?.type?.inputs ?? {});
|
||||
const outputs = from.tmp?.type?.outputs ?? [];
|
||||
smartConnect(from: NodeInstance, to: NodeInstance): Edge | undefined {
|
||||
const inputs = Object.entries(to.state?.type?.inputs ?? {});
|
||||
const outputs = from.state?.type?.outputs ?? [];
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const [inputName, input] = inputs[0];
|
||||
for (let o = 0; o < outputs.length; o++) {
|
||||
@@ -398,11 +505,11 @@ export class GraphManager extends EventEmitter<{
|
||||
return Math.max(0, ...this.nodes.keys()) + 1;
|
||||
}
|
||||
|
||||
createGraph(nodes: Node[], edges: [number, number, number, string][]) {
|
||||
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
|
||||
// map old ids to new ids
|
||||
const idMap = new Map<number, number>();
|
||||
const idMap = new SvelteMap<number, number>();
|
||||
|
||||
let startId = this.createNodeId()
|
||||
let startId = this.createNodeId();
|
||||
|
||||
nodes = nodes.map((node) => {
|
||||
const id = startId++;
|
||||
@@ -419,16 +526,14 @@ export class GraphManager extends EventEmitter<{
|
||||
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
|
||||
|
||||
if (!from || !to) {
|
||||
throw new Error("Edge references non-existing node");
|
||||
throw new Error('Edge references non-existing node');
|
||||
}
|
||||
|
||||
to.tmp = to.tmp || {};
|
||||
to.tmp.parents = to.tmp.parents || [];
|
||||
to.tmp.parents.push(from);
|
||||
to.state.parents = to.state.parents || [];
|
||||
to.state.parents.push(from);
|
||||
|
||||
from.tmp = from.tmp || {};
|
||||
from.tmp.children = from.tmp.children || [];
|
||||
from.tmp.children.push(to);
|
||||
from.state.children = from.state.children || [];
|
||||
from.state.children.push(to);
|
||||
|
||||
return [from, edge[1], to, edge[3]] as Edge;
|
||||
});
|
||||
@@ -446,11 +551,11 @@ export class GraphManager extends EventEmitter<{
|
||||
createNode({
|
||||
type,
|
||||
position,
|
||||
props = {},
|
||||
props = {}
|
||||
}: {
|
||||
type: Node["type"];
|
||||
position: Node["position"];
|
||||
props: Node["props"];
|
||||
type: NodeInstance['type'];
|
||||
position: NodeInstance['position'];
|
||||
props: NodeInstance['props'];
|
||||
}) {
|
||||
const nodeType = this.registry.getNode(type);
|
||||
if (!nodeType) {
|
||||
@@ -458,56 +563,55 @@ export class GraphManager extends EventEmitter<{
|
||||
return;
|
||||
}
|
||||
|
||||
const node: Node = $state({
|
||||
const node: NodeInstance = $state({
|
||||
id: this.createNodeId(),
|
||||
type,
|
||||
position,
|
||||
tmp: { type: nodeType },
|
||||
props,
|
||||
state: { type: nodeType },
|
||||
props
|
||||
});
|
||||
|
||||
this.nodes.set(node.id, node);
|
||||
|
||||
this.save();
|
||||
|
||||
return node
|
||||
return node;
|
||||
}
|
||||
|
||||
createEdge(
|
||||
from: Node,
|
||||
from: NodeInstance,
|
||||
fromSocket: number,
|
||||
to: Node,
|
||||
to: NodeInstance,
|
||||
toSocket: string,
|
||||
{ applyUpdate = true } = {},
|
||||
{ applyUpdate = true } = {}
|
||||
): Edge | undefined {
|
||||
|
||||
const existingEdges = this.getEdgesToNode(to);
|
||||
|
||||
// check if this exact edge already exists
|
||||
const existingEdge = existingEdges.find(
|
||||
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
|
||||
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
|
||||
);
|
||||
if (existingEdge) {
|
||||
logger.error("Edge already exists", existingEdge);
|
||||
logger.error('Edge already exists', existingEdge);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if socket types match
|
||||
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket];
|
||||
const toSocketType = [to.tmp?.type?.inputs?.[toSocket]?.type];
|
||||
if (to.tmp?.type?.inputs?.[toSocket]?.accepts) {
|
||||
toSocketType.push(...(to?.tmp?.type?.inputs?.[toSocket]?.accepts || []));
|
||||
const fromSocketType = from.state?.type?.outputs?.[fromSocket];
|
||||
const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
|
||||
if (to.state?.type?.inputs?.[toSocket]?.accepts) {
|
||||
toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
|
||||
}
|
||||
|
||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||
logger.error(
|
||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
|
||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const edgeToBeReplaced = this.edges.find(
|
||||
(e) => e[2].id === to.id && e[3] === toSocket,
|
||||
(e) => e[2].id === to.id && e[3] === toSocket
|
||||
);
|
||||
if (edgeToBeReplaced) {
|
||||
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
||||
@@ -517,13 +621,10 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
this.edges.push(edge);
|
||||
|
||||
from.tmp = from.tmp || {};
|
||||
from.tmp.children = from.tmp.children || [];
|
||||
from.tmp.children.push(to);
|
||||
|
||||
to.tmp = to.tmp || {};
|
||||
to.tmp.parents = to.tmp.parents || [];
|
||||
to.tmp.parents.push(from);
|
||||
from.state.children = from.state.children || [];
|
||||
from.state.children.push(to);
|
||||
to.state.parents = to.state.parents || [];
|
||||
to.state.parents.push(from);
|
||||
|
||||
if (applyUpdate) {
|
||||
this.save();
|
||||
@@ -537,7 +638,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const nextState = this.history.undo();
|
||||
if (nextState) {
|
||||
this._init(nextState);
|
||||
this.emit("save", this.serialize());
|
||||
this.emit('save', this.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,7 +646,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const nextState = this.history.redo();
|
||||
if (nextState) {
|
||||
this._init(nextState);
|
||||
this.emit("save", this.serialize());
|
||||
this.emit('save', this.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,22 +663,29 @@ export class GraphManager extends EventEmitter<{
|
||||
if (this.currentUndoGroup) return;
|
||||
const state = this.serialize();
|
||||
this.history.save(state);
|
||||
this.emit("save", state);
|
||||
logger.log("saving graphs", state);
|
||||
|
||||
// This is some stupid race condition where the graph-manager emits a save event
|
||||
// when the graph is not fully loaded
|
||||
if (this.nodes.size === 0 && this.edges.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('save', state);
|
||||
logger.log('saving graphs', state);
|
||||
}
|
||||
|
||||
getParentsOfNode(node: Node) {
|
||||
getParentsOfNode(node: NodeInstance) {
|
||||
const parents = [];
|
||||
const stack = node.tmp?.parents?.slice(0);
|
||||
const stack = node.state?.parents?.slice(0);
|
||||
while (stack?.length) {
|
||||
if (parents.length > 1000000) {
|
||||
logger.warn("Infinite loop detected");
|
||||
logger.warn('Infinite loop detected');
|
||||
break;
|
||||
}
|
||||
const parent = stack.pop();
|
||||
if (!parent) continue;
|
||||
parents.push(parent);
|
||||
stack.push(...(parent.tmp?.parents || []));
|
||||
stack.push(...(parent.state?.parents || []));
|
||||
}
|
||||
return parents.reverse();
|
||||
}
|
||||
@@ -585,51 +693,53 @@ export class GraphManager extends EventEmitter<{
|
||||
getPossibleNodes(socket: Socket): NodeDefinition[] {
|
||||
const allDefinitions = this.getNodeDefinitions();
|
||||
|
||||
const nodeType = socket.node.tmp?.type;
|
||||
const nodeType = socket.node.state?.type;
|
||||
if (!nodeType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const definitions = typeof socket.index === "string"
|
||||
const definitions = typeof socket.index === 'string'
|
||||
? allDefinitions.filter(s => {
|
||||
return s.outputs?.find(_s => Object
|
||||
.values(nodeType?.inputs || {})
|
||||
.map(s => s.type)
|
||||
.includes(_s as NodeInput["type"])
|
||||
)
|
||||
return s.outputs?.find(_s =>
|
||||
Object
|
||||
.values(nodeType?.inputs || {})
|
||||
.map(s => s.type)
|
||||
.includes(_s as NodeInput['type'])
|
||||
);
|
||||
})
|
||||
: allDefinitions.filter(s => Object
|
||||
.values(s.inputs ?? {})
|
||||
.find(s => {
|
||||
if (s.hidden) return false;
|
||||
if (nodeType.outputs?.includes(s.type)) {
|
||||
return true
|
||||
}
|
||||
return s.accepts?.find(a => nodeType.outputs?.includes(a))
|
||||
}))
|
||||
|
||||
return definitions
|
||||
: allDefinitions.filter(s =>
|
||||
Object
|
||||
.values(s.inputs ?? {})
|
||||
.find(s => {
|
||||
if (s.hidden) return false;
|
||||
if (nodeType.outputs?.includes(s.type)) {
|
||||
return true;
|
||||
}
|
||||
return s.accepts?.find(a => nodeType.outputs?.includes(a));
|
||||
})
|
||||
);
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
|
||||
const nodeType = node?.tmp?.type;
|
||||
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
||||
const nodeType = node?.state?.type;
|
||||
if (!nodeType) return [];
|
||||
|
||||
const sockets: [Node, string | number][] = [];
|
||||
const sockets: [NodeInstance, string | number][] = [];
|
||||
|
||||
// if index is a string, we are an input looking for outputs
|
||||
if (typeof index === "string") {
|
||||
if (typeof index === 'string') {
|
||||
// filter out self and child nodes
|
||||
const children = new Set(this.getChildren(node).map((n) => n.id));
|
||||
const children = new SvelteSet(this.getChildren(node).map((n) => n.id));
|
||||
const nodes = this.getAllNodes().filter(
|
||||
(n) => n.id !== node.id && !children.has(n.id),
|
||||
(n) => n.id !== node.id && !children.has(n.id)
|
||||
);
|
||||
|
||||
const ownType = nodeType?.inputs?.[index].type;
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeType = node?.tmp?.type;
|
||||
const nodeType = node?.state?.type;
|
||||
const inputs = nodeType?.outputs;
|
||||
if (!inputs) continue;
|
||||
for (let index = 0; index < inputs.length; index++) {
|
||||
@@ -638,34 +748,34 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof index === "number") {
|
||||
} else if (typeof index === 'number') {
|
||||
// if index is a number, we are an output looking for inputs
|
||||
|
||||
// filter out self and parent nodes
|
||||
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
|
||||
const parents = new SvelteSet(this.getParentsOfNode(node).map((n) => n.id));
|
||||
const nodes = this.getAllNodes().filter(
|
||||
(n) => n.id !== node.id && !parents.has(n.id),
|
||||
(n) => n.id !== node.id && !parents.has(n.id)
|
||||
);
|
||||
|
||||
// get edges from this socket
|
||||
const edges = new Map(
|
||||
const edges = new SvelteMap(
|
||||
this.getEdgesFromNode(node)
|
||||
.filter((e) => e[1] === index)
|
||||
.map((e) => [e[2].id, e[3]]),
|
||||
.map((e) => [e[2].id, e[3]])
|
||||
);
|
||||
|
||||
const ownType = nodeType.outputs?.[index];
|
||||
|
||||
for (const node of nodes) {
|
||||
const inputs = node?.tmp?.type?.inputs;
|
||||
const inputs = node?.state?.type?.inputs;
|
||||
if (!inputs) continue;
|
||||
for (const key in inputs) {
|
||||
const otherType = [inputs[key].type];
|
||||
otherType.push(...(inputs[key].accepts || []));
|
||||
|
||||
if (
|
||||
areSocketsCompatible(ownType, otherType) &&
|
||||
edges.get(node.id) !== key
|
||||
areSocketsCompatible(ownType, otherType)
|
||||
&& edges.get(node.id) !== key
|
||||
) {
|
||||
sockets.push([node, key]);
|
||||
}
|
||||
@@ -678,7 +788,7 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
removeEdge(
|
||||
edge: Edge,
|
||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
|
||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
|
||||
) {
|
||||
const id0 = edge[0].id;
|
||||
const sid0 = edge[1];
|
||||
@@ -686,23 +796,20 @@ export class GraphManager extends EventEmitter<{
|
||||
const sid2 = edge[3];
|
||||
|
||||
const _edge = this.edges.find(
|
||||
(e) =>
|
||||
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
|
||||
(e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
|
||||
);
|
||||
|
||||
if (!_edge) return;
|
||||
|
||||
edge[0].tmp = edge[0].tmp || {};
|
||||
if (edge[0].tmp.children) {
|
||||
edge[0].tmp.children = edge[0].tmp.children.filter(
|
||||
(n: Node) => n.id !== id2,
|
||||
if (edge[0].state.children) {
|
||||
edge[0].state.children = edge[0].state.children.filter(
|
||||
(n: NodeInstance) => n.id !== id2
|
||||
);
|
||||
}
|
||||
|
||||
edge[2].tmp = edge[2].tmp || {};
|
||||
if (edge[2].tmp.parents) {
|
||||
edge[2].tmp.parents = edge[2].tmp.parents.filter(
|
||||
(n: Node) => n.id !== id0,
|
||||
if (edge[2].state.parents) {
|
||||
edge[2].state.parents = edge[2].state.parents.filter(
|
||||
(n: NodeInstance) => n.id !== id0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -711,10 +818,9 @@ export class GraphManager extends EventEmitter<{
|
||||
this.execute();
|
||||
this.save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getEdgesToNode(node: Node) {
|
||||
getEdgesToNode(node: NodeInstance) {
|
||||
return this.edges
|
||||
.filter((edge) => edge[2].id === node.id)
|
||||
.map((edge) => {
|
||||
@@ -723,10 +829,10 @@ export class GraphManager extends EventEmitter<{
|
||||
if (!from || !to) return;
|
||||
return [from, edge[1], to, edge[3]] as const;
|
||||
})
|
||||
.filter(Boolean) as unknown as [Node, number, Node, string][];
|
||||
.filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
|
||||
}
|
||||
|
||||
getEdgesFromNode(node: Node) {
|
||||
getEdgesFromNode(node: NodeInstance) {
|
||||
return this.edges
|
||||
.filter((edge) => edge[0].id === node.id)
|
||||
.map((edge) => {
|
||||
@@ -735,6 +841,6 @@ export class GraphManager extends EventEmitter<{
|
||||
if (!from || !to) return;
|
||||
return [from, edge[1], to, edge[3]] as const;
|
||||
})
|
||||
.filter(Boolean) as unknown as [Node, number, Node, string][];
|
||||
.filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,73 @@
|
||||
import type { Node, Socket } from "@nodarium/types";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import type { GraphManager } from "../graph-manager.svelte";
|
||||
import type { OrthographicCamera } from "three";
|
||||
import type { NodeInstance, Socket } from '@nodarium/types';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import type { OrthographicCamera, Vector3 } from 'three';
|
||||
import type { GraphManager } from './graph-manager.svelte';
|
||||
|
||||
|
||||
const graphStateKey = Symbol("graph-state");
|
||||
const graphStateKey = Symbol('graph-state');
|
||||
export function getGraphState() {
|
||||
return getContext<GraphState>(graphStateKey);
|
||||
}
|
||||
export function setGraphState(graphState: GraphState) {
|
||||
return setContext(graphStateKey, graphState)
|
||||
return setContext(graphStateKey, graphState);
|
||||
}
|
||||
|
||||
const graphManagerKey = Symbol("graph-manager");
|
||||
const graphManagerKey = Symbol('graph-manager');
|
||||
export function getGraphManager() {
|
||||
return getContext<GraphManager>(graphManagerKey)
|
||||
return getContext<GraphManager>(graphManagerKey);
|
||||
}
|
||||
|
||||
export function setGraphManager(manager: GraphManager) {
|
||||
return setContext(graphManagerKey, manager);
|
||||
}
|
||||
|
||||
export class GraphState {
|
||||
type EdgeData = {
|
||||
x1: number;
|
||||
y1: number;
|
||||
points: Vector3[];
|
||||
};
|
||||
|
||||
constructor(private graph: GraphManager) { }
|
||||
export class GraphState {
|
||||
constructor(private graph: GraphManager) {
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
localStorage.setItem(
|
||||
'cameraPosition',
|
||||
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
const storedPosition = localStorage.getItem('cameraPosition');
|
||||
if (storedPosition) {
|
||||
try {
|
||||
const d = JSON.parse(storedPosition);
|
||||
this.cameraPosition[0] = d[0];
|
||||
this.cameraPosition[1] = d[1];
|
||||
this.cameraPosition[2] = d[2];
|
||||
} catch (e) {
|
||||
console.log('Failed to parsed stored camera position', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width = $state(100);
|
||||
height = $state(100);
|
||||
|
||||
hoveredEdgeId = $state<string | null>(null);
|
||||
edges = new SvelteMap<string, EdgeData>();
|
||||
|
||||
wrapper = $state<HTMLDivElement>(null!);
|
||||
rect: DOMRect = $derived(
|
||||
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0),
|
||||
(this.wrapper && this.width && this.height)
|
||||
? this.wrapper.getBoundingClientRect()
|
||||
: new DOMRect(0, 0, 0, 0)
|
||||
);
|
||||
|
||||
camera = $state<OrthographicCamera>(null!);
|
||||
cameraPosition: [number, number, number] = $state([0, 0, 4]);
|
||||
cameraPosition: [number, number, number] = $state([140, 100, 3.5]);
|
||||
|
||||
clipboard: null | {
|
||||
nodes: Node[];
|
||||
nodes: NodeInstance[];
|
||||
edges: [number, number, number, string][];
|
||||
} = null;
|
||||
|
||||
@@ -46,7 +75,7 @@ export class GraphState {
|
||||
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
|
||||
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
|
||||
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
|
||||
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2,
|
||||
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2
|
||||
]);
|
||||
|
||||
boxSelection = $state(false);
|
||||
@@ -54,8 +83,8 @@ export class GraphState {
|
||||
addMenuPosition = $state<[number, number] | null>(null);
|
||||
|
||||
snapToGrid = $state(false);
|
||||
showGrid = $state(true)
|
||||
showHelp = $state(false)
|
||||
showGrid = $state(true);
|
||||
showHelp = $state(false);
|
||||
|
||||
cameraDown = [0, 0];
|
||||
mouseDownNodeId = -1;
|
||||
@@ -71,51 +100,49 @@ export class GraphState {
|
||||
hoveredSocket = $state<Socket | null>(null);
|
||||
possibleSockets = $state<Socket[]>([]);
|
||||
possibleSocketIds = $derived(
|
||||
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
|
||||
new SvelteSet(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
|
||||
);
|
||||
|
||||
getEdges() {
|
||||
return $state.snapshot(this.edges);
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedNodes.clear();
|
||||
}
|
||||
|
||||
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
|
||||
isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
|
||||
|
||||
setCameraTransform(
|
||||
x = this.cameraPosition[0],
|
||||
y = this.cameraPosition[1],
|
||||
z = this.cameraPosition[2],
|
||||
) {
|
||||
if (this.camera) {
|
||||
this.camera.position.x = x;
|
||||
this.camera.position.z = y;
|
||||
this.camera.zoom = z;
|
||||
}
|
||||
this.cameraPosition = [x, y, z];
|
||||
localStorage.setItem("cameraPosition", JSON.stringify(this.cameraPosition));
|
||||
setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
|
||||
this.edges.set(edgeId, { x1, y1, points });
|
||||
}
|
||||
|
||||
removeEdgeGeometry(edgeId: string) {
|
||||
this.edges.delete(edgeId);
|
||||
}
|
||||
|
||||
updateNodePosition(node: Node) {
|
||||
if (node?.tmp?.ref && node?.tmp?.mesh) {
|
||||
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
|
||||
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
|
||||
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
|
||||
node.tmp.mesh.position.x = node.tmp.x + 10;
|
||||
node.tmp.mesh.position.z = node.tmp.y + this.getNodeHeight(node.type) / 2;
|
||||
if (
|
||||
node.tmp.x === node.position[0] &&
|
||||
node.tmp.y === node.position[1]
|
||||
) {
|
||||
delete node.tmp.x;
|
||||
delete node.tmp.y;
|
||||
}
|
||||
this.graph.edges = [...this.graph.edges];
|
||||
} else {
|
||||
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
|
||||
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
|
||||
node.tmp.mesh.position.x = node.position[0] + 10;
|
||||
node.tmp.mesh.position.z =
|
||||
node.position[1] + this.getNodeHeight(node.type) / 2;
|
||||
getEdgeData() {
|
||||
return this.edges;
|
||||
}
|
||||
|
||||
updateNodePosition(node: NodeInstance) {
|
||||
if (
|
||||
node.state.x === node.position[0]
|
||||
&& node.state.y === node.position[1]
|
||||
) {
|
||||
delete node.state.x;
|
||||
delete node.state.y;
|
||||
}
|
||||
|
||||
if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
|
||||
if (node.state.ref) {
|
||||
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
|
||||
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
|
||||
}
|
||||
} else {
|
||||
if (node.state.ref) {
|
||||
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
||||
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,25 +155,24 @@ export class GraphState {
|
||||
return 4;
|
||||
} else if (z > 11) {
|
||||
return 2;
|
||||
} else {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
getSocketPosition(
|
||||
node: Node,
|
||||
index: string | number,
|
||||
node: NodeInstance,
|
||||
index: string | number
|
||||
): [number, number] {
|
||||
if (typeof index === "number") {
|
||||
if (typeof index === 'number') {
|
||||
return [
|
||||
(node?.tmp?.x ?? node.position[0]) + 20,
|
||||
(node?.tmp?.y ?? node.position[1]) + 2.5 + 10 * index,
|
||||
(node?.state?.x ?? node.position[0]) + 20,
|
||||
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
|
||||
];
|
||||
} else {
|
||||
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index);
|
||||
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
||||
return [
|
||||
node?.tmp?.x ?? node.position[0],
|
||||
(node?.tmp?.y ?? node.position[1]) + 10 + 10 * _index,
|
||||
node?.state?.x ?? node.position[0],
|
||||
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -160,52 +186,26 @@ export class GraphState {
|
||||
if (!node?.inputs) {
|
||||
return 5;
|
||||
}
|
||||
const height =
|
||||
5 +
|
||||
10 *
|
||||
Object.keys(node.inputs).filter(
|
||||
(p) =>
|
||||
p !== "seed" &&
|
||||
node?.inputs &&
|
||||
!("setting" in node?.inputs?.[p]) &&
|
||||
node.inputs[p].hidden !== true,
|
||||
).length;
|
||||
const height = 5
|
||||
+ 10
|
||||
* Object.keys(node.inputs).filter(
|
||||
(p) =>
|
||||
p !== 'seed'
|
||||
&& node?.inputs
|
||||
&& !(node?.inputs?.[p] !== undefined && 'setting' in node.inputs[p])
|
||||
&& node.inputs[p].hidden !== true
|
||||
).length;
|
||||
this.nodeHeightCache[nodeTypeId] = height;
|
||||
return height;
|
||||
}
|
||||
|
||||
setNodePosition(node: Node) {
|
||||
if (node?.tmp?.ref && node?.tmp?.mesh) {
|
||||
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
|
||||
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
|
||||
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
|
||||
node.tmp.mesh.position.x = node.tmp.x + 10;
|
||||
node.tmp.mesh.position.z = node.tmp.y + this.getNodeHeight(node.type) / 2;
|
||||
if (
|
||||
node.tmp.x === node.position[0] &&
|
||||
node.tmp.y === node.position[1]
|
||||
) {
|
||||
delete node.tmp.x;
|
||||
delete node.tmp.y;
|
||||
}
|
||||
this.graph.edges = [...this.graph.edges];
|
||||
} else {
|
||||
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
|
||||
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
|
||||
node.tmp.mesh.position.x = node.position[0] + 10;
|
||||
node.tmp.mesh.position.z =
|
||||
node.position[1] + this.getNodeHeight(node.type) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
copyNodes() {
|
||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size)
|
||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
|
||||
return;
|
||||
}
|
||||
let nodes = [
|
||||
this.activeNodeId,
|
||||
...(this.selectedNodes?.values() || []),
|
||||
...(this.selectedNodes?.values() || [])
|
||||
]
|
||||
.map((id) => this.graph.getNode(id))
|
||||
.filter(b => !!b);
|
||||
@@ -215,14 +215,14 @@ export class GraphState {
|
||||
...node,
|
||||
position: [
|
||||
this.mousePosition[0] - node.position[0],
|
||||
this.mousePosition[1] - node.position[1],
|
||||
this.mousePosition[1] - node.position[1]
|
||||
],
|
||||
tmp: undefined,
|
||||
tmp: undefined
|
||||
}));
|
||||
|
||||
this.clipboard = {
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
edges: edges
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,12 +231,11 @@ export class GraphState {
|
||||
|
||||
const nodes = this.clipboard.nodes
|
||||
.map((node) => {
|
||||
node.tmp = node.tmp || {};
|
||||
node.position[0] = this.mousePosition[0] - node.position[0];
|
||||
node.position[1] = this.mousePosition[1] - node.position[1];
|
||||
return node;
|
||||
})
|
||||
.filter(Boolean) as Node[];
|
||||
.filter(Boolean) as NodeInstance[];
|
||||
|
||||
const newNodes = this.graph.createGraph(nodes, this.clipboard.edges);
|
||||
this.selectedNodes.clear();
|
||||
@@ -245,14 +244,13 @@ export class GraphState {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setDownSocket(socket: Socket) {
|
||||
this.activeSocket = socket;
|
||||
|
||||
let { node, index, position } = socket;
|
||||
|
||||
// remove existing edge
|
||||
if (typeof index === "string") {
|
||||
if (typeof index === 'string') {
|
||||
const edges = this.graph.getEdgesToNode(node);
|
||||
for (const edge of edges) {
|
||||
if (edge[3] === index) {
|
||||
@@ -269,7 +267,7 @@ export class GraphState {
|
||||
this.activeSocket = {
|
||||
node,
|
||||
index,
|
||||
position,
|
||||
position
|
||||
};
|
||||
|
||||
this.possibleSockets = this.graph
|
||||
@@ -278,32 +276,31 @@ export class GraphState {
|
||||
return {
|
||||
node,
|
||||
index,
|
||||
position: this.getSocketPosition(node, index),
|
||||
position: this.getSocketPosition(node, index)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
projectScreenToWorld(x: number, y: number): [number, number] {
|
||||
return [
|
||||
this.cameraPosition[0] +
|
||||
(x - this.width / 2) / this.cameraPosition[2],
|
||||
this.cameraPosition[1] +
|
||||
(y - this.height / 2) / this.cameraPosition[2],
|
||||
this.cameraPosition[0]
|
||||
+ (x - this.width / 2) / this.cameraPosition[2],
|
||||
this.cameraPosition[1]
|
||||
+ (y - this.height / 2) / this.cameraPosition[2]
|
||||
];
|
||||
}
|
||||
|
||||
getNodeIdFromEvent(event: MouseEvent) {
|
||||
let clickedNodeId = -1;
|
||||
|
||||
let mx = event.clientX - this.rect.x;
|
||||
let my = event.clientY - this.rect.y;
|
||||
const mx = event.clientX - this.rect.x;
|
||||
const my = event.clientY - this.rect.y;
|
||||
|
||||
if (event.button === 0) {
|
||||
// check if the clicked element is a node
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const nodeElement = event.target.closest(".node");
|
||||
const nodeId = nodeElement?.getAttribute?.("data-node-id");
|
||||
const nodeElement = event.target.closest('.node');
|
||||
const nodeId = nodeElement?.getAttribute?.('data-node-id');
|
||||
if (nodeId) {
|
||||
clickedNodeId = parseInt(nodeId, 10);
|
||||
}
|
||||
@@ -327,14 +324,18 @@ export class GraphState {
|
||||
return clickedNodeId;
|
||||
}
|
||||
|
||||
isNodeInView(node: Node) {
|
||||
isNodeInView(node: NodeInstance) {
|
||||
const height = this.getNodeHeight(node.type);
|
||||
const width = 20;
|
||||
return (
|
||||
node.position[0] > this.cameraBounds[0] - width &&
|
||||
node.position[0] < this.cameraBounds[1] &&
|
||||
node.position[1] > this.cameraBounds[2] - height &&
|
||||
node.position[1] < this.cameraBounds[3]
|
||||
node.position[0] > this.cameraBounds[0] - width
|
||||
&& node.position[0] < this.cameraBounds[1]
|
||||
&& node.position[1] > this.cameraBounds[2] - height
|
||||
&& node.position[1] < this.cameraBounds[3]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
openNodePalette() {
|
||||
this.addMenuPosition = [this.mousePosition[0], this.mousePosition[1]];
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { Edge, Node } from "@nodarium/types";
|
||||
import { onMount } from "svelte";
|
||||
import { createKeyMap } from "../../helpers/createKeyMap";
|
||||
import AddMenu from "../components/AddMenu.svelte";
|
||||
import Background from "../background/Background.svelte";
|
||||
import BoxSelection from "../components/BoxSelection.svelte";
|
||||
import EdgeEl from "../edges/Edge.svelte";
|
||||
import NodeEl from "../node/Node.svelte";
|
||||
import Camera from "../components/Camera.svelte";
|
||||
import { Canvas } from "@threlte/core";
|
||||
import HelpView from "../components/HelpView.svelte";
|
||||
import { getGraphManager, getGraphState } from "./state.svelte";
|
||||
import { HTML } from "@threlte/extras";
|
||||
import { FileDropEventManager, MouseEventManager } from "./events";
|
||||
import { maxZoom, minZoom } from "./constants";
|
||||
import type { Edge, NodeInstance } from '@nodarium/types';
|
||||
import { Canvas } from '@threlte/core';
|
||||
import { HTML } from '@threlte/extras';
|
||||
import { createKeyMap } from '../../helpers/createKeyMap';
|
||||
import Background from '../background/Background.svelte';
|
||||
import AddMenu from '../components/AddMenu.svelte';
|
||||
import BoxSelection from '../components/BoxSelection.svelte';
|
||||
import Camera from '../components/Camera.svelte';
|
||||
import HelpView from '../components/HelpView.svelte';
|
||||
import Debug from '../debug/Debug.svelte';
|
||||
import EdgeEl from '../edges/Edge.svelte';
|
||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||
import NodeEl from '../node/Node.svelte';
|
||||
import { maxZoom, minZoom } from './constants';
|
||||
import { FileDropEventManager } from './drop.events';
|
||||
import { MouseEventManager } from './mouse.events';
|
||||
|
||||
const {
|
||||
keymap,
|
||||
keymap
|
||||
}: {
|
||||
keymap: ReturnType<typeof createKeyMap>;
|
||||
} = $props();
|
||||
@@ -40,23 +41,22 @@
|
||||
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
||||
}
|
||||
|
||||
function handleNodeCreation(node: Node) {
|
||||
function handleNodeCreation(node: NodeInstance) {
|
||||
const newNode = graph.createNode({
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
props: node.props,
|
||||
props: node.props
|
||||
});
|
||||
if (!newNode) return;
|
||||
|
||||
if (graphState.activeSocket) {
|
||||
if (typeof graphState.activeSocket.index === "number") {
|
||||
const socketType =
|
||||
graphState.activeSocket.node.tmp?.type?.outputs?.[
|
||||
graphState.activeSocket.index
|
||||
];
|
||||
if (typeof graphState.activeSocket.index === 'number') {
|
||||
const socketType = graphState.activeSocket.node.state?.type?.outputs?.[
|
||||
graphState.activeSocket.index
|
||||
];
|
||||
|
||||
const input = Object.entries(newNode?.tmp?.type?.inputs || {}).find(
|
||||
(inp) => inp[1].type === socketType,
|
||||
const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
|
||||
(inp) => inp[1].type === socketType
|
||||
);
|
||||
|
||||
if (input) {
|
||||
@@ -64,18 +64,17 @@
|
||||
graphState.activeSocket.node,
|
||||
graphState.activeSocket.index,
|
||||
newNode,
|
||||
input[0],
|
||||
input[0]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const socketType =
|
||||
graphState.activeSocket.node.tmp?.type?.inputs?.[
|
||||
graphState.activeSocket.index
|
||||
];
|
||||
const socketType = graphState.activeSocket.node.state?.type?.inputs?.[
|
||||
graphState.activeSocket.index
|
||||
];
|
||||
|
||||
const output = newNode.tmp?.type?.outputs?.find((out) => {
|
||||
const output = newNode.state?.type?.outputs?.find((out) => {
|
||||
if (socketType?.type === out) return true;
|
||||
if (socketType?.accepts?.includes(out as any)) return true;
|
||||
if ((socketType?.accepts as string[])?.includes(out)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -84,7 +83,7 @@
|
||||
newNode,
|
||||
output.indexOf(output),
|
||||
graphState.activeSocket.node,
|
||||
graphState.activeSocket.index,
|
||||
graphState.activeSocket.index
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -93,26 +92,18 @@
|
||||
graphState.activeSocket = null;
|
||||
graphState.addMenuPosition = null;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem("cameraPosition")) {
|
||||
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
|
||||
if (Array.isArray(cPosition)) {
|
||||
graphState.setCameraTransform(cPosition[0], cPosition[1], cPosition[2]);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onmousemove={(ev) => mouseEvents.handleMouseMove(ev)}
|
||||
onmouseup={(ev) => mouseEvents.handleMouseUp(ev)}
|
||||
onmousemove={(ev) => mouseEvents.handleWindowMouseMove(ev)}
|
||||
onmouseup={(ev) => mouseEvents.handleWindowMouseUp(ev)}
|
||||
/>
|
||||
|
||||
<div
|
||||
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
|
||||
bind:this={graphState.wrapper}
|
||||
class="graph-wrapper"
|
||||
style="height: 100%"
|
||||
class:is-panning={graphState.isPanning}
|
||||
class:is-hovering={graphState.hoveredNodeId !== -1}
|
||||
aria-label="Graph"
|
||||
@@ -122,6 +113,7 @@
|
||||
bind:clientHeight={graphState.height}
|
||||
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
||||
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
||||
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
|
||||
{...fileDropEvents.getEventListenerProps()}
|
||||
>
|
||||
<input
|
||||
@@ -154,20 +146,18 @@
|
||||
<BoxSelection
|
||||
cameraPosition={graphState.cameraPosition}
|
||||
p1={{
|
||||
x:
|
||||
graphState.cameraPosition[0] +
|
||||
(graphState.mouseDown[0] - graphState.width / 2) /
|
||||
graphState.cameraPosition[2],
|
||||
y:
|
||||
graphState.cameraPosition[1] +
|
||||
(graphState.mouseDown[1] - graphState.height / 2) /
|
||||
graphState.cameraPosition[2],
|
||||
x: graphState.cameraPosition[0]
|
||||
+ (graphState.mouseDown[0] - graphState.width / 2)
|
||||
/ graphState.cameraPosition[2],
|
||||
y: graphState.cameraPosition[1]
|
||||
+ (graphState.mouseDown[1] - graphState.height / 2)
|
||||
/ graphState.cameraPosition[2]
|
||||
}}
|
||||
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if graph.status === "idle"}
|
||||
{#if graph.status === 'idle'}
|
||||
{#if graphState.addMenuPosition}
|
||||
<AddMenu onnode={handleNodeCreation} />
|
||||
{/if}
|
||||
@@ -182,11 +172,20 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each graph.edges as edge}
|
||||
{#each graph.edges as edge (edge)}
|
||||
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
||||
<EdgeEl z={graphState.cameraPosition[2]} {x1} {y1} {x2} {y2} />
|
||||
<EdgeEl
|
||||
id={graph.getEdgeId(edge)}
|
||||
z={graphState.cameraPosition[2]}
|
||||
{x1}
|
||||
{y1}
|
||||
{x2}
|
||||
{y2}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<Debug />
|
||||
|
||||
<HTML transform={false}>
|
||||
<div
|
||||
role="tree"
|
||||
@@ -205,9 +204,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
</HTML>
|
||||
{:else if graph.status === "loading"}
|
||||
{:else if graph.status === 'loading'}
|
||||
<span>Loading</span>
|
||||
{:else if graph.status === "error"}
|
||||
{:else if graph.status === 'error'}
|
||||
<span>Error</span>
|
||||
{/if}
|
||||
</Canvas>
|
||||
@@ -245,7 +244,7 @@
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--layer-2);
|
||||
background: var(--color-layer-2);
|
||||
opacity: 0;
|
||||
}
|
||||
input:disabled {
|
||||
@@ -265,8 +264,8 @@
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 25px);
|
||||
border: dashed 4px var(--layer-2);
|
||||
background: var(--layer-1);
|
||||
border: dashed 4px var(--color-layer-2);
|
||||
background: var(--color-layer-1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { Graph, Node, NodeRegistry } from "@nodarium/types";
|
||||
import GraphEl from "./Graph.svelte";
|
||||
import { GraphManager } from "../graph-manager.svelte";
|
||||
import { createKeyMap } from "$lib/helpers/createKeyMap";
|
||||
import { GraphState, setGraphManager, setGraphState } from "./state.svelte";
|
||||
import { setupKeymaps } from "../keymaps";
|
||||
import { createKeyMap } from '$lib/helpers/createKeyMap';
|
||||
import type { Graph, NodeInstance, NodeRegistry } from '@nodarium/types';
|
||||
import { GraphManager } from '../graph-manager.svelte';
|
||||
import { GraphState, setGraphManager, setGraphState } from '../graph-state.svelte';
|
||||
import { setupKeymaps } from '../keymaps';
|
||||
import GraphEl from './Graph.svelte';
|
||||
|
||||
type Props = {
|
||||
graph: Graph;
|
||||
graph?: Graph;
|
||||
registry: NodeRegistry;
|
||||
|
||||
settings?: Record<string, any>;
|
||||
settings?: Record<string, unknown>;
|
||||
|
||||
activeNode?: Node;
|
||||
activeNode?: NodeInstance;
|
||||
showGrid?: boolean;
|
||||
snapToGrid?: boolean;
|
||||
showHelp?: boolean;
|
||||
settingTypes?: Record<string, any>;
|
||||
settingTypes?: Record<string, unknown>;
|
||||
|
||||
onsave?: (save: Graph) => void;
|
||||
onresult?: (result: any) => void;
|
||||
onresult?: (result: unknown) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -32,11 +32,12 @@
|
||||
showHelp = $bindable(false),
|
||||
settingTypes = $bindable(),
|
||||
onsave,
|
||||
onresult,
|
||||
onresult
|
||||
}: Props = $props();
|
||||
|
||||
export const keymap = createKeyMap([]);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
export const manager = new GraphManager(registry);
|
||||
setGraphManager(manager);
|
||||
|
||||
@@ -66,22 +67,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (settingTypes && settings) {
|
||||
manager.setSettings(settings);
|
||||
}
|
||||
});
|
||||
|
||||
manager.on("settings", (_settings) => {
|
||||
manager.on('settings', (_settings) => {
|
||||
settingTypes = { ...settingTypes, ..._settings.types };
|
||||
settings = _settings.values;
|
||||
});
|
||||
|
||||
manager.on("result", (result) => onresult?.(result));
|
||||
manager.on('result', (result) => onresult?.(result));
|
||||
|
||||
manager.on("save", (save) => onsave?.(save));
|
||||
manager.on('save', (save) => onsave?.(save));
|
||||
|
||||
manager.load(graph);
|
||||
$effect(() => {
|
||||
if (graph) {
|
||||
manager.load(graph);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<GraphEl {keymap} />
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||
import { Color, LinearSRGBColorSpace } from "three";
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import { Color, LinearSRGBColorSpace } from 'three';
|
||||
|
||||
const variables = [
|
||||
"layer-0",
|
||||
"layer-1",
|
||||
"layer-2",
|
||||
"layer-3",
|
||||
"outline",
|
||||
"active",
|
||||
"selected",
|
||||
"edge",
|
||||
'layer-0',
|
||||
'layer-1',
|
||||
'layer-2',
|
||||
'layer-3',
|
||||
'outline',
|
||||
'active',
|
||||
'selected',
|
||||
'edge'
|
||||
] as const;
|
||||
|
||||
function getColor(variable: (typeof variables)[number]) {
|
||||
const style = getComputedStyle(document.body.parentElement!);
|
||||
let color = style.getPropertyValue(`--${variable}`);
|
||||
const color = style.getPropertyValue(`--color-${variable}`);
|
||||
return new Color().setStyle(color, LinearSRGBColorSpace);
|
||||
}
|
||||
|
||||
export const colors = Object.fromEntries(
|
||||
variables.map((v) => [v, getColor(v)]),
|
||||
variables.map((v) => [v, getColor(v)])
|
||||
) as Record<(typeof variables)[number], Color>;
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
if (!appSettings.value.theme || !("getComputedStyle" in globalThis)) return;
|
||||
if (!appSettings.value.theme || !('getComputedStyle' in globalThis)) return;
|
||||
const style = getComputedStyle(document.body.parentElement!);
|
||||
for (const v of variables) {
|
||||
const hex = style.getPropertyValue(`--${v}`);
|
||||
const hex = style.getPropertyValue(`--color-${v}`);
|
||||
colors[v].setStyle(hex, LinearSRGBColorSpace);
|
||||
}
|
||||
});
|
||||
|
||||
109
app/src/lib/graph-interface/graph/drop.events.ts
Normal file
109
app/src/lib/graph-interface/graph/drop.events.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { GraphSchema, type NodeId } from '@nodarium/types';
|
||||
import type { GraphManager } from '../graph-manager.svelte';
|
||||
import type { GraphState } from '../graph-state.svelte';
|
||||
|
||||
export class FileDropEventManager {
|
||||
constructor(
|
||||
private graph: GraphManager,
|
||||
private state: GraphState
|
||||
) {}
|
||||
|
||||
handleFileDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.state.isDragging = false;
|
||||
if (!event.dataTransfer) return;
|
||||
const nodeId = event.dataTransfer.getData('data/node-id') as NodeId;
|
||||
let mx = event.clientX - this.state.rect.x;
|
||||
let my = event.clientY - this.state.rect.y;
|
||||
|
||||
if (nodeId) {
|
||||
const nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
|
||||
const nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
|
||||
if (nodeOffsetX && nodeOffsetY) {
|
||||
mx += parseInt(nodeOffsetX);
|
||||
my += parseInt(nodeOffsetY);
|
||||
}
|
||||
|
||||
let props = {};
|
||||
const rawNodeProps = event.dataTransfer.getData('data/node-props');
|
||||
if (rawNodeProps) {
|
||||
try {
|
||||
props = JSON.parse(rawNodeProps);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse node dropped', e);
|
||||
}
|
||||
}
|
||||
|
||||
const pos = this.state.projectScreenToWorld(mx, my);
|
||||
this.graph.registry.load([nodeId]).then(() => {
|
||||
this.graph.createNode({
|
||||
type: nodeId,
|
||||
props,
|
||||
position: pos
|
||||
});
|
||||
});
|
||||
} else if (event.dataTransfer.files.length) {
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
if (file.type === 'application/wasm') {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const buffer = e.target?.result;
|
||||
if (buffer?.constructor === ArrayBuffer) {
|
||||
const nodeType = await this.graph.registry.register(nodeId, buffer);
|
||||
|
||||
this.graph.createNode({
|
||||
type: nodeType.id,
|
||||
props: {},
|
||||
position: this.state.projectScreenToWorld(mx, my)
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
} else if (file.type === 'application/json') {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const buffer = e.target?.result as ArrayBuffer;
|
||||
if (buffer) {
|
||||
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
||||
this.graph.load(state);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave() {
|
||||
this.state.isDragging = false;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
handleDragEnter(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
handleDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
handleDragEnd(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
getEventListenerProps() {
|
||||
return {
|
||||
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
||||
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
||||
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
||||
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
||||
onmouseleave: () => this.handleMouseLeave()
|
||||
};
|
||||
}
|
||||
}
|
||||
117
app/src/lib/graph-interface/graph/edge.events.ts
Normal file
117
app/src/lib/graph-interface/graph/edge.events.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { Box } from '@nodarium/types';
|
||||
import type { GraphManager } from '../graph-manager.svelte';
|
||||
import type { GraphState } from '../graph-state.svelte';
|
||||
import { distanceFromPointToSegment } from '../helpers';
|
||||
|
||||
export class EdgeInteractionManager {
|
||||
constructor(
|
||||
private graph: GraphManager,
|
||||
private state: GraphState
|
||||
) {}
|
||||
|
||||
private MIN_DISTANCE = 3;
|
||||
|
||||
private _boundingBoxes = new Map<string, Box>();
|
||||
|
||||
handleMouseDown() {
|
||||
this._boundingBoxes.clear();
|
||||
|
||||
const possibleEdges = this.graph
|
||||
.getPossibleDropOnEdges(this.state.activeNodeId)
|
||||
.map(e => this.graph.getEdgeId(e));
|
||||
|
||||
const edges = this.state.getEdges();
|
||||
for (const edge of edges) {
|
||||
const edgeId = edge[0];
|
||||
if (!possibleEdges.includes(edgeId)) {
|
||||
edges.delete(edgeId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [edgeId, data] of edges) {
|
||||
const points = data.points;
|
||||
let minX = points[0].x + data.x1;
|
||||
let maxX = points[0].x + data.x1;
|
||||
let minY = points[0].z + data.y1;
|
||||
let maxY = points[0].z + data.y1;
|
||||
|
||||
// Iterate through all points to find the true bounds
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const x = data.x1 + points[i].x;
|
||||
const y = data.y1 + points[i].z;
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
|
||||
const boundingBox = {
|
||||
minX: minX - this.MIN_DISTANCE,
|
||||
maxX: maxX + this.MIN_DISTANCE,
|
||||
minY: minY - this.MIN_DISTANCE,
|
||||
maxY: maxY + this.MIN_DISTANCE
|
||||
};
|
||||
|
||||
this._boundingBoxes.set(edgeId, boundingBox);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove() {
|
||||
const [mouseX, mouseY] = this.state.mousePosition;
|
||||
const hoveredEdgeIds: string[] = [];
|
||||
|
||||
const edges = this.state.getEdges();
|
||||
|
||||
// Check if mouse is inside any bounding box
|
||||
for (const [edgeId, box] of this._boundingBoxes) {
|
||||
const isInside = mouseX >= box.minX
|
||||
&& mouseX <= box.maxX
|
||||
&& mouseY >= box.minY
|
||||
&& mouseY <= box.maxY;
|
||||
|
||||
if (isInside) {
|
||||
hoveredEdgeIds.push(edgeId);
|
||||
}
|
||||
}
|
||||
|
||||
let hoveredEdgeId: string | null = null;
|
||||
let hoveredEdgeDistance = Infinity;
|
||||
|
||||
const DENSITY = 10; // higher DENSITY = less points checked (yes density might not be the best name :-)
|
||||
for (const edgeId of hoveredEdgeIds) {
|
||||
const edge = edges.get(edgeId)!;
|
||||
for (let i = 0; i < edge.points.length - DENSITY; i += DENSITY) {
|
||||
const pointAx = edge.points[i].x + edge.x1;
|
||||
const pointAy = edge.points[i].z + edge.y1;
|
||||
const pointBx = edge.points[i + DENSITY].x + edge.x1;
|
||||
const pointBy = edge.points[i + DENSITY].z + edge.y1;
|
||||
const distance = distanceFromPointToSegment(
|
||||
pointAx,
|
||||
pointAy,
|
||||
pointBx,
|
||||
pointBy,
|
||||
mouseX,
|
||||
mouseY
|
||||
);
|
||||
if (distance < this.MIN_DISTANCE) {
|
||||
if (distance < hoveredEdgeDistance) {
|
||||
hoveredEdgeDistance = distance;
|
||||
hoveredEdgeId = edgeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.state.hoveredEdgeId = hoveredEdgeId;
|
||||
}
|
||||
|
||||
handleMouseUp() {
|
||||
if (this.state.hoveredEdgeId) {
|
||||
const edge = this.graph.getEdgeById(this.state.hoveredEdgeId);
|
||||
if (edge) {
|
||||
this.graph.dropNodeOnEdge(this.state.activeNodeId, edge);
|
||||
}
|
||||
this.state.hoveredEdgeId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +1,23 @@
|
||||
import { GraphSchema, type NodeType, type Node } from "@nodarium/types";
|
||||
import type { GraphManager } from "../graph-manager.svelte";
|
||||
import type { GraphState } from "./state.svelte";
|
||||
import { animate, lerp } from "$lib/helpers";
|
||||
import { snapToGrid as snapPointToGrid } from "../helpers";
|
||||
import { maxZoom, minZoom, zoomSpeed } from "./constants";
|
||||
|
||||
|
||||
export class FileDropEventManager {
|
||||
|
||||
constructor(
|
||||
private graph: GraphManager,
|
||||
private state: GraphState
|
||||
) { }
|
||||
|
||||
handleFileDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.state.isDragging = false;
|
||||
if (!event.dataTransfer) return;
|
||||
const nodeId = event.dataTransfer.getData("data/node-id") as NodeType;
|
||||
let mx = event.clientX - this.state.rect.x;
|
||||
let my = event.clientY - this.state.rect.y;
|
||||
|
||||
if (nodeId) {
|
||||
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
|
||||
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
|
||||
if (nodeOffsetX && nodeOffsetY) {
|
||||
mx += parseInt(nodeOffsetX);
|
||||
my += parseInt(nodeOffsetY);
|
||||
}
|
||||
|
||||
let props = {};
|
||||
let rawNodeProps = event.dataTransfer.getData("data/node-props");
|
||||
if (rawNodeProps) {
|
||||
try {
|
||||
props = JSON.parse(rawNodeProps);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
const pos = this.state.projectScreenToWorld(mx, my);
|
||||
this.graph.registry.load([nodeId]).then(() => {
|
||||
this.graph.createNode({
|
||||
type: nodeId,
|
||||
props,
|
||||
position: pos,
|
||||
});
|
||||
});
|
||||
} else if (event.dataTransfer.files.length) {
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
if (file.type === "application/wasm") {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const buffer = e.target?.result;
|
||||
if (buffer?.constructor === ArrayBuffer) {
|
||||
const nodeType = await this.graph.registry.register(buffer);
|
||||
|
||||
this.graph.createNode({
|
||||
type: nodeType.id,
|
||||
props: {},
|
||||
position: this.state.projectScreenToWorld(mx, my),
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
} else if (file.type === "application/json") {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const buffer = e.target?.result as ArrayBuffer;
|
||||
if (buffer) {
|
||||
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
||||
this.graph.load(state);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave() {
|
||||
this.state.isDragging = false;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
handleDragEnter(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
handleDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
handleDragEnd(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
this.state.isPanning = false;
|
||||
}
|
||||
|
||||
getEventListenerProps() {
|
||||
return {
|
||||
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
||||
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
||||
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
||||
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
||||
onmouseleave: () => this.handleMouseLeave(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import { animate, lerp } from '$lib/helpers';
|
||||
import { type NodeInstance } from '@nodarium/types';
|
||||
import type { GraphManager } from '../graph-manager.svelte';
|
||||
import { type GraphState } from '../graph-state.svelte';
|
||||
import { snapToGrid as snapPointToGrid } from '../helpers';
|
||||
import { maxZoom, minZoom, zoomSpeed } from './constants';
|
||||
import { EdgeInteractionManager } from './edge.events';
|
||||
|
||||
export class MouseEventManager {
|
||||
|
||||
edgeInteractionManager: EdgeInteractionManager;
|
||||
|
||||
constructor(
|
||||
private graph: GraphManager,
|
||||
private state: GraphState
|
||||
) { }
|
||||
) {
|
||||
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
|
||||
}
|
||||
|
||||
|
||||
handleMouseUp(event: MouseEvent) {
|
||||
handleWindowMouseUp(event: MouseEvent) {
|
||||
this.edgeInteractionManager.handleMouseUp();
|
||||
this.state.isPanning = false;
|
||||
if (!this.state.mouseDown) return;
|
||||
|
||||
@@ -131,45 +27,43 @@ export class MouseEventManager {
|
||||
|
||||
if (clickedNodeId !== -1) {
|
||||
if (activeNode) {
|
||||
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
||||
if (!activeNode?.state?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
||||
this.state.activeNodeId = clickedNodeId;
|
||||
this.state.clearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeNode?.tmp?.isMoving) {
|
||||
activeNode.tmp = activeNode.tmp || {};
|
||||
activeNode.tmp.isMoving = false;
|
||||
if (activeNode?.state?.isMoving) {
|
||||
activeNode.state = activeNode.state || {};
|
||||
activeNode.state.isMoving = false;
|
||||
if (this.state.snapToGrid) {
|
||||
const snapLevel = this.state.getSnapLevel();
|
||||
activeNode.position[0] = snapPointToGrid(
|
||||
activeNode?.tmp?.x ?? activeNode.position[0],
|
||||
5 / snapLevel,
|
||||
activeNode?.state?.x ?? activeNode.position[0],
|
||||
5 / snapLevel
|
||||
);
|
||||
activeNode.position[1] = snapPointToGrid(
|
||||
activeNode?.tmp?.y ?? activeNode.position[1],
|
||||
5 / snapLevel,
|
||||
activeNode?.state?.y ?? activeNode.position[1],
|
||||
5 / snapLevel
|
||||
);
|
||||
} else {
|
||||
activeNode.position[0] = activeNode?.tmp?.x ?? activeNode.position[0];
|
||||
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
|
||||
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
||||
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
||||
}
|
||||
const nodes = [
|
||||
...[...(this.state.selectedNodes?.values() || [])].map((id) =>
|
||||
this.graph.getNode(id),
|
||||
),
|
||||
] as Node[];
|
||||
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
|
||||
] as NodeInstance[];
|
||||
|
||||
const vec = [
|
||||
activeNode.position[0] - (activeNode?.tmp.x || 0),
|
||||
activeNode.position[1] - (activeNode?.tmp.y || 0),
|
||||
activeNode.position[0] - (activeNode?.state.x || 0),
|
||||
activeNode.position[1] - (activeNode?.state.y || 0)
|
||||
];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (!node) continue;
|
||||
node.tmp = node.tmp || {};
|
||||
const { x, y } = node.tmp;
|
||||
node.state = node.state || {};
|
||||
const { x, y } = node.state;
|
||||
if (x !== undefined && y !== undefined) {
|
||||
node.position[0] = x + vec[0];
|
||||
node.position[1] = y + vec[1];
|
||||
@@ -179,14 +73,14 @@ export class MouseEventManager {
|
||||
animate(500, (a: number) => {
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
node?.tmp &&
|
||||
node.tmp["x"] !== undefined &&
|
||||
node.tmp["y"] !== undefined
|
||||
node?.state
|
||||
&& node.state['x'] !== undefined
|
||||
&& node.state['y'] !== undefined
|
||||
) {
|
||||
node.tmp.x = lerp(node.tmp.x, node.position[0], a);
|
||||
node.tmp.y = lerp(node.tmp.y, node.position[1], a);
|
||||
node.state.x = lerp(node.state.x, node.position[0], a);
|
||||
node.state.y = lerp(node.state.y, node.position[1], a);
|
||||
this.state.updateNodePosition(node);
|
||||
if (node?.tmp?.isMoving) {
|
||||
if (node?.state?.isMoving) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -195,24 +89,24 @@ export class MouseEventManager {
|
||||
this.graph.save();
|
||||
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
||||
if (
|
||||
typeof this.state.hoveredSocket.index === "number" &&
|
||||
typeof this.state.activeSocket.index === "string"
|
||||
typeof this.state.hoveredSocket.index === 'number'
|
||||
&& typeof this.state.activeSocket.index === 'string'
|
||||
) {
|
||||
this.graph.createEdge(
|
||||
this.state.hoveredSocket.node,
|
||||
this.state.hoveredSocket.index || 0,
|
||||
this.state.activeSocket.node,
|
||||
this.state.activeSocket.index,
|
||||
this.state.activeSocket.index
|
||||
);
|
||||
} else if (
|
||||
typeof this.state.activeSocket.index == "number" &&
|
||||
typeof this.state.hoveredSocket.index === "string"
|
||||
typeof this.state.activeSocket.index == 'number'
|
||||
&& typeof this.state.hoveredSocket.index === 'string'
|
||||
) {
|
||||
this.graph.createEdge(
|
||||
this.state.activeSocket.node,
|
||||
this.state.activeSocket.index || 0,
|
||||
this.state.hoveredSocket.node,
|
||||
this.state.hoveredSocket.index,
|
||||
this.state.hoveredSocket.index
|
||||
);
|
||||
}
|
||||
this.graph.save();
|
||||
@@ -220,18 +114,18 @@ export class MouseEventManager {
|
||||
// Handle automatic adding of nodes on ctrl+mouseUp
|
||||
this.state.edgeEndPosition = [
|
||||
this.state.mousePosition[0],
|
||||
this.state.mousePosition[1],
|
||||
this.state.mousePosition[1]
|
||||
];
|
||||
|
||||
if (typeof this.state.activeSocket.index === "number") {
|
||||
if (typeof this.state.activeSocket.index === 'number') {
|
||||
this.state.addMenuPosition = [
|
||||
this.state.mousePosition[0],
|
||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
||||
];
|
||||
} else {
|
||||
this.state.addMenuPosition = [
|
||||
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
|
||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
||||
];
|
||||
}
|
||||
return;
|
||||
@@ -239,11 +133,11 @@ export class MouseEventManager {
|
||||
|
||||
// check if camera moved
|
||||
if (
|
||||
clickedNodeId === -1 &&
|
||||
!this.state.boxSelection &&
|
||||
this.state.cameraDown[0] === this.state.cameraPosition[0] &&
|
||||
this.state.cameraDown[1] === this.state.cameraPosition[1] &&
|
||||
this.state.isBodyFocused()
|
||||
clickedNodeId === -1
|
||||
&& !this.state.boxSelection
|
||||
&& this.state.cameraDown[0] === this.state.cameraPosition[0]
|
||||
&& this.state.cameraDown[1] === this.state.cameraPosition[1]
|
||||
&& this.state.isBodyFocused()
|
||||
) {
|
||||
this.state.activeNodeId = -1;
|
||||
this.state.clearSelection();
|
||||
@@ -257,23 +151,34 @@ export class MouseEventManager {
|
||||
this.state.addMenuPosition = null;
|
||||
}
|
||||
|
||||
handleContextMenu(event: MouseEvent) {
|
||||
if (!this.state.addMenuPosition) {
|
||||
event.preventDefault();
|
||||
this.state.openNodePalette();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown(event: MouseEvent) {
|
||||
// Right click
|
||||
if (event.button === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.mouseDown) return;
|
||||
this.state.edgeEndPosition = null;
|
||||
|
||||
if (event.target instanceof HTMLElement) {
|
||||
if (
|
||||
event.target.nodeName !== "CANVAS" &&
|
||||
!event.target.classList.contains("node") &&
|
||||
!event.target.classList.contains("content")
|
||||
event.target.nodeName !== 'CANVAS'
|
||||
&& !event.target.classList.contains('node')
|
||||
&& !event.target.classList.contains('content')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mx = event.clientX - this.state.rect.x;
|
||||
let my = event.clientY - this.state.rect.y;
|
||||
const mx = event.clientX - this.state.rect.x;
|
||||
const my = event.clientY - this.state.rect.y;
|
||||
|
||||
this.state.mouseDown = [mx, my];
|
||||
this.state.cameraDown[0] = this.state.cameraPosition[0];
|
||||
@@ -288,7 +193,7 @@ export class MouseEventManager {
|
||||
this.state.activeNodeId = clickedNodeId;
|
||||
// if the selected node is the same as the clicked node
|
||||
} else if (this.state.activeNodeId === clickedNodeId) {
|
||||
//$activeNodeId = -1;
|
||||
// $activeNodeId = -1;
|
||||
// if the clicked node is different from the selected node and secondary
|
||||
} else if (event.ctrlKey) {
|
||||
this.state.selectedNodes.add(this.state.activeNodeId);
|
||||
@@ -312,33 +217,33 @@ export class MouseEventManager {
|
||||
this.state.activeNodeId = clickedNodeId;
|
||||
this.state.clearSelection();
|
||||
}
|
||||
this.edgeInteractionManager.handleMouseDown();
|
||||
} else if (event.ctrlKey) {
|
||||
this.state.boxSelection = true;
|
||||
}
|
||||
|
||||
const node = this.graph.getNode(this.state.activeNodeId);
|
||||
if (!node) return;
|
||||
node.tmp = node.tmp || {};
|
||||
node.tmp.downX = node.position[0];
|
||||
node.tmp.downY = node.position[1];
|
||||
node.state = node.state || {};
|
||||
node.state.downX = node.position[0];
|
||||
node.state.downY = node.position[1];
|
||||
|
||||
if (this.state.selectedNodes) {
|
||||
for (const nodeId of this.state.selectedNodes) {
|
||||
const n = this.graph.getNode(nodeId);
|
||||
if (!n) continue;
|
||||
n.tmp = n.tmp || {};
|
||||
n.tmp.downX = n.position[0];
|
||||
n.tmp.downY = n.position[1];
|
||||
n.state = n.state || {};
|
||||
n.state.downX = n.position[0];
|
||||
n.state.downY = n.position[1];
|
||||
}
|
||||
}
|
||||
|
||||
this.state.edgeEndPosition = null;
|
||||
}
|
||||
|
||||
|
||||
handleMouseMove(event: MouseEvent) {
|
||||
let mx = event.clientX - this.state.rect.x;
|
||||
let my = event.clientY - this.state.rect.y;
|
||||
handleWindowMouseMove(event: MouseEvent) {
|
||||
const mx = event.clientX - this.state.rect.x;
|
||||
const my = event.clientY - this.state.rect.y;
|
||||
|
||||
this.state.mousePosition = this.state.projectScreenToWorld(mx, my);
|
||||
this.state.hoveredNodeId = this.state.getNodeIdFromEvent(event);
|
||||
@@ -351,8 +256,8 @@ export class MouseEventManager {
|
||||
let _socket;
|
||||
for (const socket of this.state.possibleSockets) {
|
||||
const dist = Math.sqrt(
|
||||
(socket.position[0] - this.state.mousePosition[0]) ** 2 +
|
||||
(socket.position[1] - this.state.mousePosition[1]) ** 2,
|
||||
(socket.position[0] - this.state.mousePosition[0]) ** 2
|
||||
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
|
||||
);
|
||||
if (dist < smallestDist) {
|
||||
smallestDist = dist;
|
||||
@@ -375,14 +280,14 @@ export class MouseEventManager {
|
||||
event.stopPropagation();
|
||||
const mouseD = this.state.projectScreenToWorld(
|
||||
this.state.mouseDown[0],
|
||||
this.state.mouseDown[1],
|
||||
this.state.mouseDown[1]
|
||||
);
|
||||
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
||||
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
||||
const y1 = Math.min(mouseD[1], this.state.mousePosition[1]);
|
||||
const y2 = Math.max(mouseD[1], this.state.mousePosition[1]);
|
||||
for (const node of this.graph.nodes.values()) {
|
||||
if (!node?.tmp) continue;
|
||||
if (!node?.state) continue;
|
||||
const x = node.position[0];
|
||||
const y = node.position[1];
|
||||
const height = this.state.getNodeHeight(node.type);
|
||||
@@ -397,18 +302,17 @@ export class MouseEventManager {
|
||||
|
||||
// here we are handling dragging of nodes
|
||||
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
|
||||
this.edgeInteractionManager.handleMouseMove();
|
||||
const node = this.graph.getNode(this.state.activeNodeId);
|
||||
if (!node || event.buttons !== 1) return;
|
||||
|
||||
node.tmp = node.tmp || {};
|
||||
node.state = node.state || {};
|
||||
|
||||
const oldX = node.tmp.downX || 0;
|
||||
const oldY = node.tmp.downY || 0;
|
||||
const oldX = node.state.downX || 0;
|
||||
const oldY = node.state.downY || 0;
|
||||
|
||||
let newX =
|
||||
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY =
|
||||
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
|
||||
if (event.ctrlKey) {
|
||||
const snapLevel = this.state.getSnapLevel();
|
||||
@@ -418,10 +322,10 @@ export class MouseEventManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.tmp.isMoving) {
|
||||
if (!node.state.isMoving) {
|
||||
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
|
||||
if (dist > 0.2) {
|
||||
node.tmp.isMoving = true;
|
||||
node.state.isMoving = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,15 +335,15 @@ export class MouseEventManager {
|
||||
if (this.state.selectedNodes?.size) {
|
||||
for (const nodeId of this.state.selectedNodes) {
|
||||
const n = this.graph.getNode(nodeId);
|
||||
if (!n?.tmp) continue;
|
||||
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
|
||||
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
|
||||
if (!n?.state) continue;
|
||||
n.state.x = (n?.state?.downX || 0) - vecX;
|
||||
n.state.y = (n?.state?.downY || 0) - vecY;
|
||||
this.state.updateNodePosition(n);
|
||||
}
|
||||
}
|
||||
|
||||
node.tmp.x = newX;
|
||||
node.tmp.y = newY;
|
||||
node.state.x = newX;
|
||||
node.state.y = newY;
|
||||
|
||||
this.state.updateNodePosition(node);
|
||||
|
||||
@@ -448,22 +352,19 @@ export class MouseEventManager {
|
||||
|
||||
// here we are handling panning of camera
|
||||
this.state.isPanning = true;
|
||||
let newX =
|
||||
this.state.cameraDown[0] -
|
||||
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY =
|
||||
this.state.cameraDown[1] -
|
||||
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
const newX = this.state.cameraDown[0]
|
||||
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
const newY = this.state.cameraDown[1]
|
||||
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
|
||||
this.state.setCameraTransform(newX, newY);
|
||||
this.state.cameraPosition[0] = newX;
|
||||
this.state.cameraPosition[1] = newY;
|
||||
}
|
||||
|
||||
|
||||
handleMouseScroll(event: WheelEvent) {
|
||||
const bodyIsFocused =
|
||||
document.activeElement === document.body ||
|
||||
document.activeElement === this.state.wrapper ||
|
||||
document?.activeElement?.id === "graph";
|
||||
const bodyIsFocused = document.activeElement === document.body
|
||||
|| document.activeElement === this.state.wrapper
|
||||
|| document?.activeElement?.id === 'graph';
|
||||
if (!bodyIsFocused) return;
|
||||
|
||||
// Define zoom speed and clamp it between -1 and 1
|
||||
@@ -478,23 +379,20 @@ export class MouseEventManager {
|
||||
maxZoom,
|
||||
isNegative
|
||||
? this.state.cameraPosition[2] / delta
|
||||
: this.state.cameraPosition[2] * delta,
|
||||
),
|
||||
: this.state.cameraPosition[2] * delta
|
||||
)
|
||||
);
|
||||
|
||||
// Calculate the ratio of the new zoom to the original zoom
|
||||
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
||||
|
||||
// Update camera position and zoom level
|
||||
this.state.setCameraTransform(
|
||||
this.state.mousePosition[0] -
|
||||
(this.state.mousePosition[0] - this.state.cameraPosition[0]) /
|
||||
zoomRatio,
|
||||
this.state.mousePosition[1] -
|
||||
(this.state.mousePosition[1] - this.state.cameraPosition[1]) /
|
||||
zoomRatio,
|
||||
newZoom,
|
||||
);
|
||||
this.state.cameraPosition[0] = this.state.mousePosition[0]
|
||||
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
|
||||
/ zoomRatio;
|
||||
this.state.cameraPosition[1] = this.state.mousePosition[1]
|
||||
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
|
||||
/ zoomRatio;
|
||||
this.state.cameraPosition[2] = newZoom;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import throttle from "$lib/helpers/throttle";
|
||||
import throttle from '$lib/helpers/throttle';
|
||||
|
||||
type EventMap = Record<string, unknown>;
|
||||
type EventKey<T extends EventMap> = string & keyof T;
|
||||
type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown;
|
||||
|
||||
export default class EventEmitter<
|
||||
T extends EventMap = { [key: string]: unknown },
|
||||
T extends EventMap = { [key: string]: unknown }
|
||||
> {
|
||||
index = 0;
|
||||
public eventMap: T = {} as T;
|
||||
@@ -32,11 +32,11 @@ export default class EventEmitter<
|
||||
public on<K extends EventKey<T>>(
|
||||
event: K,
|
||||
cb: EventReceiver<T[K]>,
|
||||
throttleTimer = 0,
|
||||
throttleTimer = 0
|
||||
) {
|
||||
if (throttleTimer > 0) cb = throttle(cb, throttleTimer);
|
||||
const cbs = Object.assign(this.cbs, {
|
||||
[event]: [...(this.cbs[event] || []), cb],
|
||||
[event]: [...(this.cbs[event] || []), cb]
|
||||
});
|
||||
this.cbs = cbs;
|
||||
|
||||
@@ -54,10 +54,10 @@ export default class EventEmitter<
|
||||
*/
|
||||
public once<K extends EventKey<T>>(
|
||||
event: K,
|
||||
cb: EventReceiver<T[K]>,
|
||||
cb: EventReceiver<T[K]>
|
||||
): () => void {
|
||||
const cbsOnce = Object.assign(this.cbsOnce, {
|
||||
[event]: [...(this.cbsOnce[event] || []), cb],
|
||||
[event]: [...(this.cbsOnce[event] || []), cb]
|
||||
});
|
||||
this.cbsOnce = cbsOnce;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
|
||||
|
||||
export function animate(
|
||||
duration: number,
|
||||
callback: (progress: number) => void | false,
|
||||
callback: (progress: number) => void | false
|
||||
) {
|
||||
const start = performance.now();
|
||||
const loop = (time: number) => {
|
||||
@@ -33,59 +33,52 @@ export function createNodePath({
|
||||
cornerBottom = 0,
|
||||
leftBump = false,
|
||||
rightBump = false,
|
||||
aspectRatio = 1,
|
||||
aspectRatio = 1
|
||||
} = {}) {
|
||||
return `M0,${cornerTop}
|
||||
${
|
||||
cornerTop
|
||||
? ` V${cornerTop}
|
||||
cornerTop
|
||||
? ` V${cornerTop}
|
||||
Q0,0 ${cornerTop * aspectRatio},0
|
||||
H${100 - cornerTop * aspectRatio}
|
||||
Q100,0 100,${cornerTop}
|
||||
`
|
||||
: ` V0
|
||||
: ` V0
|
||||
H100
|
||||
`
|
||||
}
|
||||
}
|
||||
V${y - height / 2}
|
||||
${
|
||||
rightBump
|
||||
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||
: ` H100`
|
||||
}
|
||||
rightBump
|
||||
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||
: ` H100`
|
||||
}
|
||||
${
|
||||
cornerBottom
|
||||
? ` V${100 - cornerBottom}
|
||||
cornerBottom
|
||||
? ` V${100 - cornerBottom}
|
||||
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
||||
H${cornerBottom * aspectRatio}
|
||||
Q0,100 0,${100 - cornerBottom}
|
||||
`
|
||||
: `${leftBump ? `V100 H0` : `V100`}`
|
||||
}
|
||||
: `${leftBump ? `V100 H0` : `V100`}`
|
||||
}
|
||||
${
|
||||
leftBump
|
||||
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
||||
: ` H0`
|
||||
}
|
||||
Z`.replace(/\s+/g, " ");
|
||||
leftBump
|
||||
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
|
||||
y - height / 2
|
||||
}`
|
||||
: ` H0`
|
||||
}
|
||||
Z`.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
export const debounce = (fn: Function, ms = 300) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return function (this: any, ...args: any[]) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
};
|
||||
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
|
||||
? globalThis.structuredClone
|
||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export const clone: <T>(v: T) => T =
|
||||
"structedClone" in globalThis
|
||||
? globalThis.structuredClone
|
||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export function withSubComponents<A, B extends Record<string, any>>(
|
||||
export function withSubComponents<A, B extends Record<string, unknown>>(
|
||||
component: A,
|
||||
subcomponents: B,
|
||||
subcomponents: B
|
||||
): A & B {
|
||||
Object.keys(subcomponents).forEach((key) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -93,3 +86,27 @@ export function withSubComponents<A, B extends Record<string, any>>(
|
||||
});
|
||||
return component as A & B;
|
||||
}
|
||||
|
||||
export function distanceFromPointToSegment(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
x0: number,
|
||||
y0: number
|
||||
): number {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
|
||||
if (dx === 0 && dy === 0) {
|
||||
return Math.hypot(x0 - x1, y0 - y1);
|
||||
}
|
||||
|
||||
const t = ((x0 - x1) * dx + (y0 - y1) * dy) / (dx * dx + dy * dy);
|
||||
const clampedT = Math.max(0, Math.min(1, t));
|
||||
|
||||
const px = x1 + clampedT * dx;
|
||||
const py = y1 + clampedT * dy;
|
||||
|
||||
return Math.hypot(x0 - px, y0 - py);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { type Writable, writable } from 'svelte/store';
|
||||
|
||||
function isStore(v: unknown): v is Writable<unknown> {
|
||||
return v !== null && typeof v === "object" && "subscribe" in v && "set" in v;
|
||||
return v !== null && typeof v === 'object' && 'subscribe' in v && 'set' in v;
|
||||
}
|
||||
|
||||
const storeIds: Map<string, ReturnType<typeof createLocalStore>> = new Map();
|
||||
|
||||
const HAS_LOCALSTORAGE = "localStorage" in globalThis;
|
||||
const HAS_LOCALSTORAGE = 'localStorage' in globalThis;
|
||||
|
||||
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||
|
||||
let store: Writable<T>;
|
||||
|
||||
if (HAS_LOCALSTORAGE) {
|
||||
@@ -36,18 +35,15 @@ function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||
subscribe: store.subscribe,
|
||||
set: store.set,
|
||||
update: store.update
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
||||
|
||||
if (storeIds.has(key)) return storeIds.get(key) as Writable<T>;
|
||||
|
||||
const store = createLocalStore(key, initialValue)
|
||||
const store = createLocalStore(key, initialValue);
|
||||
|
||||
storeIds.set(key, store);
|
||||
|
||||
return store
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { create, type Delta } from "jsondiffpatch";
|
||||
import type { Graph } from "@nodarium/types";
|
||||
import { clone } from "./helpers/index.js";
|
||||
import { createLogger } from "@nodarium/utils";
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import { createLogger } from '@nodarium/utils';
|
||||
import { create, type Delta } from 'jsondiffpatch';
|
||||
import { clone } from './helpers/index.js';
|
||||
|
||||
const diff = create({
|
||||
objectHash: function (obj, index) {
|
||||
objectHash: function(obj, index) {
|
||||
if (obj === null) return obj;
|
||||
if ("id" in obj) return obj.id as string;
|
||||
if ("_id" in obj) return obj._id as string;
|
||||
if ('id' in obj) return obj.id as string;
|
||||
if ('_id' in obj) return obj._id as string;
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.join("-");
|
||||
return obj.join('-');
|
||||
}
|
||||
return "$$index:" + index;
|
||||
},
|
||||
return '$$index:' + index;
|
||||
}
|
||||
});
|
||||
|
||||
const log = createLogger("history");
|
||||
const log = createLogger('history');
|
||||
log.mute();
|
||||
|
||||
export class HistoryManager {
|
||||
@@ -26,7 +26,7 @@ export class HistoryManager {
|
||||
|
||||
private opts = {
|
||||
debounce: 400,
|
||||
maxHistory: 100,
|
||||
maxHistory: 100
|
||||
};
|
||||
|
||||
constructor({ maxHistory = 100, debounce = 100 } = {}) {
|
||||
@@ -40,12 +40,12 @@ export class HistoryManager {
|
||||
if (!this.state) {
|
||||
this.state = clone(state);
|
||||
this.initialState = this.state;
|
||||
log.log("initial state saved");
|
||||
log.log('initial state saved');
|
||||
} else {
|
||||
const newState = state;
|
||||
const delta = diff.diff(this.state, newState);
|
||||
if (delta) {
|
||||
log.log("saving state");
|
||||
log.log('saving state');
|
||||
// Add the delta to history
|
||||
if (this.index < this.history.length - 1) {
|
||||
// Clear the history after the current index if new changes are made
|
||||
@@ -61,7 +61,7 @@ export class HistoryManager {
|
||||
}
|
||||
this.state = newState;
|
||||
} else {
|
||||
log.log("no changes");
|
||||
log.log('no changes');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export class HistoryManager {
|
||||
|
||||
undo() {
|
||||
if (this.index === -1 && this.initialState) {
|
||||
log.log("reached start, loading initial state");
|
||||
log.log('reached start, loading initial state');
|
||||
return clone(this.initialState);
|
||||
} else {
|
||||
const delta = this.history[this.index];
|
||||
@@ -95,7 +95,7 @@ export class HistoryManager {
|
||||
this.state = nextState;
|
||||
return clone(nextState);
|
||||
} else {
|
||||
log.log("reached end");
|
||||
log.log('reached end');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { animate, lerp } from "$lib/helpers";
|
||||
import type { createKeyMap } from "$lib/helpers/createKeyMap";
|
||||
import FileSaver from "file-saver";
|
||||
import type { GraphManager } from "./graph-manager.svelte";
|
||||
import type { GraphState } from "./graph/state.svelte";
|
||||
import { animate, lerp } from '$lib/helpers';
|
||||
import type { createKeyMap } from '$lib/helpers/createKeyMap';
|
||||
import { panelState } from '$lib/sidebar/PanelState.svelte';
|
||||
import FileSaver from 'file-saver';
|
||||
import type { GraphManager } from './graph-manager.svelte';
|
||||
import type { GraphState } from './graph-state.svelte';
|
||||
|
||||
type Keymap = ReturnType<typeof createKeyMap>;
|
||||
export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) {
|
||||
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "l",
|
||||
description: "Select linked nodes",
|
||||
key: 'l',
|
||||
description: 'Select linked nodes',
|
||||
callback: () => {
|
||||
const activeNode = graph.getNode(graphState.activeNodeId);
|
||||
if (activeNode) {
|
||||
@@ -20,56 +19,52 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
||||
graphState.selectedNodes.add(node.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "?",
|
||||
description: "Toggle Help",
|
||||
key: '?',
|
||||
description: 'Toggle Help',
|
||||
callback: () => {
|
||||
// TODO: fix this
|
||||
// showHelp = !showHelp;
|
||||
},
|
||||
panelState.setActivePanel('shortcuts');
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "c",
|
||||
key: 'c',
|
||||
ctrl: true,
|
||||
description: "Copy active nodes",
|
||||
callback: () => graphState.copyNodes(),
|
||||
description: 'Copy active nodes',
|
||||
callback: () => graphState.copyNodes()
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "v",
|
||||
key: 'v',
|
||||
ctrl: true,
|
||||
description: "Paste nodes",
|
||||
callback: () => graphState.pasteNodes(),
|
||||
description: 'Paste nodes',
|
||||
callback: () => graphState.pasteNodes()
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "Escape",
|
||||
description: "Deselect nodes",
|
||||
key: 'Escape',
|
||||
description: 'Deselect nodes',
|
||||
callback: () => {
|
||||
graphState.activeNodeId = -1;
|
||||
graphState.clearSelection();
|
||||
graphState.edgeEndPosition = null;
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "A",
|
||||
key: 'A',
|
||||
shift: true,
|
||||
description: "Add new Node",
|
||||
callback: () => {
|
||||
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
|
||||
},
|
||||
description: 'Add new Node',
|
||||
callback: () => graphState.openNodePalette()
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: ".",
|
||||
description: "Center camera",
|
||||
key: '.',
|
||||
description: 'Center camera',
|
||||
callback: () => {
|
||||
if (!graphState.isBodyFocused()) return;
|
||||
|
||||
@@ -88,71 +83,69 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
||||
const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
|
||||
|
||||
animate(500, (a: number) => {
|
||||
graphState.setCameraTransform(
|
||||
lerp(camX, average[0], ease(a)),
|
||||
lerp(camY, average[1], ease(a)),
|
||||
lerp(camZ, 2, ease(a)),
|
||||
);
|
||||
graphState.cameraPosition[0] = lerp(camX, average[0], ease(a));
|
||||
graphState.cameraPosition[1] = lerp(camY, average[1], ease(a));
|
||||
graphState.cameraPosition[2] = lerp(camZ, 2, ease(a));
|
||||
if (graphState.mouseDown) return false;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "a",
|
||||
key: 'a',
|
||||
ctrl: true,
|
||||
preventDefault: true,
|
||||
description: "Select all nodes",
|
||||
description: 'Select all nodes',
|
||||
callback: () => {
|
||||
if (!graphState.isBodyFocused()) return;
|
||||
for (const node of graph.nodes.keys()) {
|
||||
graphState.selectedNodes.add(node);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "z",
|
||||
key: 'z',
|
||||
ctrl: true,
|
||||
description: "Undo",
|
||||
description: 'Undo',
|
||||
callback: () => {
|
||||
if (!graphState.isBodyFocused()) return;
|
||||
graph.undo();
|
||||
for (const node of graph.nodes.values()) {
|
||||
graphState.updateNodePosition(node);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "y",
|
||||
key: 'y',
|
||||
ctrl: true,
|
||||
description: "Redo",
|
||||
description: 'Redo',
|
||||
callback: () => {
|
||||
graph.redo();
|
||||
for (const node of graph.nodes.values()) {
|
||||
graphState.updateNodePosition(node);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "s",
|
||||
key: 's',
|
||||
ctrl: true,
|
||||
description: "Save",
|
||||
description: 'Save',
|
||||
preventDefault: true,
|
||||
callback: () => {
|
||||
const state = graph.serialize();
|
||||
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({
|
||||
key: ["Delete", "Backspace", "x"],
|
||||
description: "Delete selected nodes",
|
||||
key: ['Delete', 'Backspace', 'x'],
|
||||
description: 'Delete selected nodes',
|
||||
callback: (event) => {
|
||||
if (!graphState.isBodyFocused()) return;
|
||||
graph.startUndoGroup();
|
||||
@@ -173,20 +166,18 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
||||
graphState.clearSelection();
|
||||
}
|
||||
graph.saveUndoGroup();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "f",
|
||||
description: "Smart Connect Nodes",
|
||||
key: 'f',
|
||||
description: 'Smart Connect Nodes',
|
||||
callback: () => {
|
||||
const nodes = [...graphState.selectedNodes.values()]
|
||||
.map((g) => graph.getNode(g))
|
||||
.filter((n) => !!n);
|
||||
const edge = graph.smartConnect(nodes[0], nodes[1]);
|
||||
if (!edge) graph.smartConnect(nodes[1], nodes[0]);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { Node } from "@nodarium/types";
|
||||
import { onMount } from "svelte";
|
||||
import { getGraphState } from "../graph/state.svelte";
|
||||
import { T } from "@threlte/core";
|
||||
import { type Mesh } from "three";
|
||||
import NodeFrag from "./Node.frag";
|
||||
import NodeVert from "./Node.vert";
|
||||
import NodeHtml from "./NodeHTML.svelte";
|
||||
import { colors } from "../graph/colors.svelte";
|
||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import type { NodeInstance } from '@nodarium/types';
|
||||
import { T } from '@threlte/core';
|
||||
import { type Mesh } from 'three';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
import { colors } from '../graph/colors.svelte';
|
||||
import NodeFrag from './Node.frag';
|
||||
import NodeVert from './Node.vert';
|
||||
import NodeHtml from './NodeHTML.svelte';
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
type Props = {
|
||||
node: Node;
|
||||
node: NodeInstance;
|
||||
inView: boolean;
|
||||
z: number;
|
||||
};
|
||||
let { node, inView, z }: Props = $props();
|
||||
let { node = $bindable(), inView, z }: Props = $props();
|
||||
|
||||
const isActive = $derived(graphState.activeNodeId === node.id);
|
||||
const isSelected = $derived(graphState.selectedNodes.has(node.id));
|
||||
const strokeColor = $derived(
|
||||
appSettings.value.theme &&
|
||||
(isSelected
|
||||
appSettings.value.theme
|
||||
&& (isSelected
|
||||
? colors.selected
|
||||
: isActive
|
||||
? colors.active
|
||||
: colors.outline),
|
||||
? colors.active
|
||||
: colors.outline)
|
||||
);
|
||||
|
||||
let meshRef: Mesh | undefined = $state();
|
||||
@@ -35,17 +34,16 @@
|
||||
const height = graphState.getNodeHeight(node.type);
|
||||
|
||||
$effect(() => {
|
||||
if (!node.tmp) node.tmp = {};
|
||||
if (meshRef && !node.tmp?.mesh) {
|
||||
node.tmp.mesh = meshRef;
|
||||
if (meshRef && !node.state?.mesh) {
|
||||
node.state.mesh = meshRef;
|
||||
graphState.updateNodePosition(node);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<T.Mesh
|
||||
position.x={node.position[0] + 10}
|
||||
position.z={node.position[1] + height / 2}
|
||||
position.x={(node.state.x ?? node.position[0]) + 10}
|
||||
position.z={(node.state.y ?? node.position[1]) + height / 2}
|
||||
position.y={0.8}
|
||||
rotation.x={-Math.PI / 2}
|
||||
bind:ref={meshRef}
|
||||
@@ -57,16 +55,16 @@
|
||||
fragmentShader={NodeFrag}
|
||||
transparent
|
||||
uniforms={{
|
||||
uColorBright: { value: colors["layer-2"] },
|
||||
uColorDark: { value: colors["layer-1"] },
|
||||
uColorBright: { value: colors['layer-2'] },
|
||||
uColorDark: { value: colors['layer-1'] },
|
||||
uStrokeColor: { value: colors.outline.clone() },
|
||||
uStrokeWidth: { value: 1.0 },
|
||||
uWidth: { value: 20 },
|
||||
uHeight: { value: height },
|
||||
uHeight: { value: height }
|
||||
}}
|
||||
uniforms.uStrokeColor.value={strokeColor.clone()}
|
||||
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
||||
/>
|
||||
</T.Mesh>
|
||||
|
||||
<NodeHtml {node} {inView} {isActive} {isSelected} {z} />
|
||||
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} />
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Node } from "@nodarium/types";
|
||||
import NodeHeader from "./NodeHeader.svelte";
|
||||
import NodeParameter from "./NodeParameter.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { getGraphState } from "../graph/state.svelte";
|
||||
import type { NodeInstance } from '@nodarium/types';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
import NodeHeader from './NodeHeader.svelte';
|
||||
import NodeParameter from './NodeParameter.svelte';
|
||||
|
||||
let ref: HTMLDivElement;
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
type Props = {
|
||||
node: Node;
|
||||
position?: "absolute" | "fixed" | "relative";
|
||||
node: NodeInstance;
|
||||
position?: 'absolute' | 'fixed' | 'relative';
|
||||
isActive?: boolean;
|
||||
isSelected?: boolean;
|
||||
inView?: boolean;
|
||||
@@ -20,26 +19,26 @@
|
||||
|
||||
let {
|
||||
node = $bindable(),
|
||||
position = "absolute",
|
||||
position = 'absolute',
|
||||
isActive = false,
|
||||
isSelected = false,
|
||||
inView = true,
|
||||
z = 2,
|
||||
z = 2
|
||||
}: Props = $props();
|
||||
|
||||
// If we dont have a random offset, all nodes becom visible at the same zoom level -> stuttering
|
||||
const zOffset = Math.random() - 0.5;
|
||||
const zLimit = 2 - zOffset;
|
||||
|
||||
const parameters = Object.entries(node?.tmp?.type?.inputs || {}).filter(
|
||||
(p) =>
|
||||
p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
|
||||
const parameters = Object.entries(node.state?.type?.inputs || {}).filter(
|
||||
(p) => p[1].type !== 'seed' && !('setting' in p[1]) && p[1]?.hidden !== true
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
node.tmp = node.tmp || {};
|
||||
node.tmp.ref = ref;
|
||||
graphState?.updateNodePosition(node);
|
||||
$effect(() => {
|
||||
if ('state' in node && !node.state.ref) {
|
||||
node.state.ref = ref;
|
||||
graphState?.updateNodePosition(node);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -47,7 +46,7 @@
|
||||
class="node {position}"
|
||||
class:active={isActive}
|
||||
style:--cz={z + zOffset}
|
||||
style:display={inView && z > zLimit ? "block" : "none"}
|
||||
style:display={inView && z > zLimit ? 'block' : 'none'}
|
||||
class:selected={isSelected}
|
||||
class:out-of-view={!inView}
|
||||
data-node-id={node.id}
|
||||
@@ -56,7 +55,7 @@
|
||||
>
|
||||
<NodeHeader {node} />
|
||||
|
||||
{#each parameters as [key, value], i}
|
||||
{#each parameters as [key, value], i (key)}
|
||||
<NodeParameter
|
||||
bind:node
|
||||
id={key}
|
||||
@@ -72,22 +71,22 @@
|
||||
user-select: none !important;
|
||||
cursor: pointer;
|
||||
width: 200px;
|
||||
color: var(--text-color);
|
||||
color: var(--color-text);
|
||||
transform: translate3d(var(--nx), var(--ny), 0);
|
||||
z-index: 1;
|
||||
opacity: calc((var(--cz) - 2.5) / 3.5);
|
||||
font-weight: 300;
|
||||
--stroke: var(--outline);
|
||||
--stroke: var(--color-outline);
|
||||
--stroke-width: 2px;
|
||||
}
|
||||
|
||||
.node.active {
|
||||
--stroke: var(--active);
|
||||
--stroke: var(--color-active);
|
||||
--stroke-width: 2px;
|
||||
}
|
||||
|
||||
.node.selected {
|
||||
--stroke: var(--selected);
|
||||
--stroke: var(--color-selected);
|
||||
--stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,62 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { getGraphState } from "../graph/state.svelte.js";
|
||||
import { createNodePath } from "../helpers/index.js";
|
||||
import type { Node } from "@nodarium/types";
|
||||
import type { NodeInstance } from '@nodarium/types';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
import { createNodePath } from '../helpers/index.js';
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
const { node }: { node: Node } = $props();
|
||||
const { node }: { node: NodeInstance } = $props();
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
graphState.setDownSocket?.({
|
||||
node,
|
||||
index: 0,
|
||||
position: graphState.getSocketPosition?.(node, 0),
|
||||
});
|
||||
if ('state' in node) {
|
||||
graphState.setDownSocket?.({
|
||||
node,
|
||||
index: 0,
|
||||
position: graphState.getSocketPosition?.(node, 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const cornerTop = 10;
|
||||
const rightBump = !!node?.tmp?.type?.outputs?.length;
|
||||
const rightBump = $derived(!!node?.state?.type?.outputs?.length);
|
||||
const aspectRatio = 0.25;
|
||||
|
||||
const path = createNodePath({
|
||||
depth: 5.5,
|
||||
height: 34,
|
||||
y: 49,
|
||||
cornerTop,
|
||||
rightBump,
|
||||
aspectRatio,
|
||||
});
|
||||
// const pathDisabled = createNodePath({
|
||||
// depth: 0,
|
||||
// height: 15,
|
||||
// y: 50,
|
||||
// cornerTop,
|
||||
// rightBump,
|
||||
// aspectRatio,
|
||||
// });
|
||||
const pathHover = createNodePath({
|
||||
depth: 8.5,
|
||||
height: 50,
|
||||
y: 49,
|
||||
cornerTop,
|
||||
rightBump,
|
||||
aspectRatio,
|
||||
});
|
||||
const path = $derived(
|
||||
createNodePath({
|
||||
depth: 5.5,
|
||||
height: 34,
|
||||
y: 49,
|
||||
cornerTop,
|
||||
rightBump,
|
||||
aspectRatio
|
||||
})
|
||||
);
|
||||
const pathHover = $derived(
|
||||
createNodePath({
|
||||
depth: 8.5,
|
||||
height: 50,
|
||||
y: 49,
|
||||
cornerTop,
|
||||
rightBump,
|
||||
aspectRatio
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
|
||||
<div class="content">
|
||||
{node.type.split("/").pop()}
|
||||
{node.type.split('/').pop()}
|
||||
</div>
|
||||
<div
|
||||
class="click-target"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onmousedown={handleMouseDown}
|
||||
></div>
|
||||
>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
@@ -68,8 +67,7 @@
|
||||
--hover-path: path("${pathHover}");
|
||||
`}
|
||||
>
|
||||
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"
|
||||
></path>
|
||||
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -110,10 +108,8 @@
|
||||
|
||||
svg path {
|
||||
stroke-width: 0.2px;
|
||||
transition:
|
||||
d 0.3s ease,
|
||||
fill 0.3s ease;
|
||||
fill: var(--layer-2);
|
||||
transition: d 0.3s ease, fill 0.3s ease;
|
||||
fill: var(--color-layer-2);
|
||||
stroke: var(--stroke);
|
||||
stroke-width: var(--stroke-width);
|
||||
d: var(--path);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Node, NodeInput } from "@nodarium/types";
|
||||
import { Input } from "@nodarium/ui";
|
||||
import type { GraphManager } from "../graph-manager.svelte";
|
||||
import type { NodeInput, NodeInstance } from '@nodarium/types';
|
||||
import { Input } from '@nodarium/ui';
|
||||
import type { GraphManager } from '../graph-manager.svelte';
|
||||
|
||||
type Props = {
|
||||
node: Node;
|
||||
node: NodeInstance;
|
||||
input: NodeInput;
|
||||
id: string;
|
||||
elementId?: string;
|
||||
@@ -16,17 +16,18 @@
|
||||
input,
|
||||
id,
|
||||
elementId = `input-${Math.random().toString(36).substring(7)}`,
|
||||
graph,
|
||||
graph
|
||||
}: Props = $props();
|
||||
|
||||
function getDefaultValue() {
|
||||
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
|
||||
if ("value" in input && input?.value !== undefined)
|
||||
if ('value' in input && input?.value !== undefined) {
|
||||
return input?.value as number;
|
||||
if (input.type === "boolean") return 0;
|
||||
if (input.type === "float") return 0.5;
|
||||
if (input.type === "integer") return 0;
|
||||
if (input.type === "select") return 0;
|
||||
}
|
||||
if (input.type === 'boolean') return 0;
|
||||
if (input.type === 'float') return 0.5;
|
||||
if (input.type === 'integer') return 0;
|
||||
if (input.type === 'select') return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
NodeInput as NodeInputType,
|
||||
Node as NodeType,
|
||||
} from "@nodarium/types";
|
||||
import { createNodePath } from "../helpers/index.js";
|
||||
import NodeInput from "./NodeInput.svelte";
|
||||
import { getGraphManager, getGraphState } from "../graph/state.svelte.js";
|
||||
import type { NodeInput, NodeInstance } from '@nodarium/types';
|
||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||
import { createNodePath } from '../helpers';
|
||||
import NodeInputEl from './NodeInput.svelte';
|
||||
|
||||
type Props = {
|
||||
node: NodeType;
|
||||
input: NodeInputType;
|
||||
node: NodeInstance;
|
||||
input: NodeInput;
|
||||
id: string;
|
||||
isLast?: boolean;
|
||||
};
|
||||
@@ -18,9 +15,9 @@
|
||||
|
||||
let { node = $bindable(), input, id, isLast }: Props = $props();
|
||||
|
||||
const inputType = node?.tmp?.type?.inputs?.[id]!;
|
||||
const inputType = $derived(node?.state?.type?.inputs?.[id]);
|
||||
|
||||
const socketId = `${node.id}-${id}`;
|
||||
const socketId = $derived(`${node.id}-${id}`);
|
||||
|
||||
const graphState = getGraphState();
|
||||
const graphId = graph?.id;
|
||||
@@ -33,38 +30,44 @@
|
||||
graphState.setDownSocket({
|
||||
node,
|
||||
index: id,
|
||||
position: graphState.getSocketPosition?.(node, id),
|
||||
position: graphState.getSocketPosition?.(node, id)
|
||||
});
|
||||
}
|
||||
|
||||
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true;
|
||||
const cornerBottom = isLast ? 5 : 0;
|
||||
const leftBump = $derived(node.state?.type?.inputs?.[id].internal !== true);
|
||||
const cornerBottom = $derived(isLast ? 5 : 0);
|
||||
const aspectRatio = 0.5;
|
||||
|
||||
const path = createNodePath({
|
||||
depth: 7,
|
||||
height: 20,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio,
|
||||
});
|
||||
const pathDisabled = createNodePath({
|
||||
depth: 6,
|
||||
height: 18,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio,
|
||||
});
|
||||
const pathHover = createNodePath({
|
||||
depth: 8,
|
||||
height: 25,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio,
|
||||
});
|
||||
const path = $derived(
|
||||
createNodePath({
|
||||
depth: 7,
|
||||
height: 20,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio
|
||||
})
|
||||
);
|
||||
const pathDisabled = $derived(
|
||||
createNodePath({
|
||||
depth: 6,
|
||||
height: 18,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio
|
||||
})
|
||||
);
|
||||
const pathHover = $derived(
|
||||
createNodePath({
|
||||
depth: 8,
|
||||
height: 25,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -75,15 +78,19 @@
|
||||
>
|
||||
{#key id && graphId}
|
||||
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
|
||||
{#if inputType.label !== ""}
|
||||
<label for={elementId}>{input.label || id}</label>
|
||||
{#if inputType?.label !== ''}
|
||||
<label for={elementId} title={input.description}>{input.label || id}</label>
|
||||
{/if}
|
||||
{#if inputType.external !== true}
|
||||
<NodeInput {graph} {elementId} bind:node {input} {id} />
|
||||
<span
|
||||
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
|
||||
title={JSON.stringify(input, null, 2)}
|
||||
></span>
|
||||
{#if inputType?.external !== true}
|
||||
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
|
||||
{#if node?.state?.type?.inputs?.[id]?.internal !== true}
|
||||
<div data-node-socket class="large target"></div>
|
||||
<div
|
||||
data-node-socket
|
||||
@@ -91,7 +98,8 @@
|
||||
onmousedown={handleMouseDown}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
></div>
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
@@ -166,10 +174,8 @@
|
||||
}
|
||||
|
||||
svg path {
|
||||
transition:
|
||||
d 0.3s ease,
|
||||
fill 0.3s ease;
|
||||
fill: var(--layer-1);
|
||||
transition: d 0.3s ease, fill 0.3s ease;
|
||||
fill: var(--color-layer-1);
|
||||
stroke: var(--stroke);
|
||||
stroke-width: var(--stroke-width);
|
||||
d: var(--path);
|
||||
@@ -184,9 +190,6 @@
|
||||
.content.disabled {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.content.disabled > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.disabled svg path {
|
||||
d: var(--hover-path-disabled) !important;
|
||||
|
||||
@@ -1 +1,95 @@
|
||||
{"settings":{"resolution.circle":26,"resolution.curve":39},"nodes":[{"id":9,"position":[220,80],"type":"max/plantarium/output","props":{}},{"id":10,"position":[95,80],"type":"max/plantarium/stem","props":{"amount":5,"length":11,"thickness":0.1}},{"id":14,"position":[195,80],"type":"max/plantarium/gravity","props":{"strength":0.38,"scale":39,"fixBottom":0,"directionalStrength":[1,1,1],"depth":1,"curviness":1}},{"id":15,"position":[120,80],"type":"max/plantarium/noise","props":{"strength":4.9,"scale":2.2,"fixBottom":1,"directionalStrength":[1,1,1],"depth":1,"octaves":1}},{"id":16,"position":[70,80],"type":"max/plantarium/vec3","props":{"0":0,"1":0,"2":0}},{"id":17,"position":[45,80],"type":"max/plantarium/random","props":{"min":-2,"max":2}},{"id":18,"position":[170,80],"type":"max/plantarium/branch","props":{"length":1.6,"thickness":0.69,"amount":36,"offsetSingle":0.5,"lowestBranch":0.46,"highestBranch":1,"depth":1,"rotation":180}},{"id":19,"position":[145,80],"type":"max/plantarium/gravity","props":{"strength":0.38,"scale":39,"fixBottom":0,"directionalStrength":[1,1,1],"depth":1,"curviness":1}},{"id":20,"position":[70,120],"type":"max/plantarium/random","props":{"min":0.073,"max":0.15}}],"edges":[[14,0,9,"input"],[10,0,15,"plant"],[16,0,10,"origin"],[17,0,16,"0"],[17,0,16,"2"],[18,0,14,"plant"],[15,0,19,"plant"],[19,0,18,"plant"],[20,0,10,"thickness"]]}
|
||||
{
|
||||
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
|
||||
"nodes": [
|
||||
{ "id": 9, "position": [220, 80], "type": "max/plantarium/output", "props": {} },
|
||||
{
|
||||
"id": 10,
|
||||
"position": [95, 80],
|
||||
"type": "max/plantarium/stem",
|
||||
"props": { "amount": 5, "length": 11, "thickness": 0.1 }
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"position": [195, 80],
|
||||
"type": "max/plantarium/gravity",
|
||||
"props": {
|
||||
"strength": 0.38,
|
||||
"scale": 39,
|
||||
"fixBottom": 0,
|
||||
"directionalStrength": [1, 1, 1],
|
||||
"depth": 1,
|
||||
"curviness": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"position": [120, 80],
|
||||
"type": "max/plantarium/noise",
|
||||
"props": {
|
||||
"strength": 4.9,
|
||||
"scale": 2.2,
|
||||
"fixBottom": 1,
|
||||
"directionalStrength": [1, 1, 1],
|
||||
"depth": 1,
|
||||
"octaves": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"position": [70, 80],
|
||||
"type": "max/plantarium/vec3",
|
||||
"props": { "0": 0, "1": 0, "2": 0 }
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"position": [45, 80],
|
||||
"type": "max/plantarium/random",
|
||||
"props": { "min": -2, "max": 2 }
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"position": [170, 80],
|
||||
"type": "max/plantarium/branch",
|
||||
"props": {
|
||||
"length": 1.6,
|
||||
"thickness": 0.69,
|
||||
"amount": 36,
|
||||
"offsetSingle": 0.5,
|
||||
"lowestBranch": 0.46,
|
||||
"highestBranch": 1,
|
||||
"depth": 1,
|
||||
"rotation": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"position": [145, 80],
|
||||
"type": "max/plantarium/gravity",
|
||||
"props": {
|
||||
"strength": 0.38,
|
||||
"scale": 39,
|
||||
"fixBottom": 0,
|
||||
"directionalStrength": [1, 1, 1],
|
||||
"depth": 1,
|
||||
"curviness": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"position": [70, 120],
|
||||
"type": "max/plantarium/random",
|
||||
"props": { "min": 0.073, "max": 0.15 }
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
[14, 0, 9, "input"],
|
||||
[10, 0, 15, "plant"],
|
||||
[16, 0, 10, "origin"],
|
||||
[17, 0, 16, "0"],
|
||||
[17, 0, 16, "2"],
|
||||
[18, 0, 14, "plant"],
|
||||
[15, 0, 19, "plant"],
|
||||
[19, 0, 18, "plant"],
|
||||
[20, 0, 10, "thickness"]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Graph } from "@nodarium/types";
|
||||
import type { Graph } from '@nodarium/types';
|
||||
|
||||
export function grid(width: number, height: number) {
|
||||
|
||||
const graph: Graph = {
|
||||
id: Math.floor(Math.random() * 100000),
|
||||
edges: [],
|
||||
nodes: [],
|
||||
nodes: []
|
||||
};
|
||||
|
||||
const amount = width * height;
|
||||
@@ -16,27 +15,20 @@ export function grid(width: number, height: number) {
|
||||
|
||||
graph.nodes.push({
|
||||
id: i,
|
||||
tmp: {
|
||||
visible: false,
|
||||
},
|
||||
position: [x * 30, y * 40],
|
||||
props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 },
|
||||
type: i == 0 ? "max/plantarium/float" : "max/plantarium/math",
|
||||
type: i == 0 ? 'max/plantarium/float' : 'max/plantarium/math'
|
||||
});
|
||||
|
||||
graph.edges.push([i, 0, i + 1, i === amount - 1 ? "input" : "a",]);
|
||||
graph.edges.push([i, 0, i + 1, i === amount - 1 ? 'input' : 'a']);
|
||||
}
|
||||
|
||||
graph.nodes.push({
|
||||
id: amount,
|
||||
tmp: {
|
||||
visible: false,
|
||||
},
|
||||
position: [width * 30, (height - 1) * 40],
|
||||
type: "max/plantarium/output",
|
||||
props: {},
|
||||
type: 'max/plantarium/output',
|
||||
props: {}
|
||||
});
|
||||
|
||||
return graph;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export { grid } from "./grid";
|
||||
export { tree } from "./tree";
|
||||
export { plant } from "./plant";
|
||||
export { default as lottaFaces } from "./lotta-faces.json";
|
||||
export { default as lottaNodes } from "./lotta-nodes.json";
|
||||
export { default as defaultPlant } from "./default.json"
|
||||
export { default as lottaNodesAndFaces } from "./lotta-nodes-and-faces.json";
|
||||
|
||||
export { default as defaultPlant } from './default.json';
|
||||
export { grid } from './grid';
|
||||
export { default as lottaFaces } from './lotta-faces.json';
|
||||
export { default as lottaNodesAndFaces } from './lotta-nodes-and-faces.json';
|
||||
export { default as lottaNodes } from './lotta-nodes.json';
|
||||
export { plant } from './plant';
|
||||
export { tree } from './tree';
|
||||
|
||||
@@ -1 +1,44 @@
|
||||
{"settings":{"resolution.circle":64,"resolution.curve":64,"randomSeed":false},"nodes":[{"id":9,"position":[260,0],"type":"max/plantarium/output","props":{}},{"id":18,"position":[185,0],"type":"max/plantarium/stem","props":{"amount":64,"length":12,"thickness":0.15}},{"id":19,"position":[210,0],"type":"max/plantarium/noise","props":{"scale":1.3,"strength":5.4}},{"id":20,"position":[235,0],"type":"max/plantarium/branch","props":{"length":0.8,"thickness":0.8,"amount":3}},{"id":21,"position":[160,0],"type":"max/plantarium/vec3","props":{"0":0.39,"1":0,"2":0.41}},{"id":22,"position":[130,0],"type":"max/plantarium/random","props":{"min":-2,"max":2}}],"edges":[[18,0,19,"plant"],[19,0,20,"plant"],[20,0,9,"input"],[21,0,18,"origin"],[22,0,21,"0"],[22,0,21,"2"]]}
|
||||
{
|
||||
"settings": { "resolution.circle": 64, "resolution.curve": 64, "randomSeed": false },
|
||||
"nodes": [
|
||||
{ "id": 9, "position": [260, 0], "type": "max/plantarium/output", "props": {} },
|
||||
{
|
||||
"id": 18,
|
||||
"position": [185, 0],
|
||||
"type": "max/plantarium/stem",
|
||||
"props": { "amount": 64, "length": 12, "thickness": 0.15 }
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"position": [210, 0],
|
||||
"type": "max/plantarium/noise",
|
||||
"props": { "scale": 1.3, "strength": 5.4 }
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"position": [235, 0],
|
||||
"type": "max/plantarium/branch",
|
||||
"props": { "length": 0.8, "thickness": 0.8, "amount": 3 }
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"position": [160, 0],
|
||||
"type": "max/plantarium/vec3",
|
||||
"props": { "0": 0.39, "1": 0, "2": 0.41 }
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"position": [130, 0],
|
||||
"type": "max/plantarium/random",
|
||||
"props": { "min": -2, "max": 2 }
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
[18, 0, 19, "plant"],
|
||||
[19, 0, 20, "plant"],
|
||||
[20, 0, 9, "input"],
|
||||
[21, 0, 18, "origin"],
|
||||
[22, 0, 21, "0"],
|
||||
[22, 0, 21, "2"]
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,12 +1,71 @@
|
||||
export const plant = {
|
||||
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
|
||||
"nodes": [
|
||||
{ "id": 9, "position": [180, 80], "type": "max/plantarium/output", "props": {} },
|
||||
{ "id": 10, "position": [55, 80], "type": "max/plantarium/stem", "props": { "amount": 1, "length": 11, "thickness": 0.71 } },
|
||||
{ "id": 11, "position": [80, 80], "type": "max/plantarium/noise", "props": { "strength": 35, "scale": 4.6, "fixBottom": 1, "directionalStrength": [1, 0.74, 0.083], "depth": 1 } },
|
||||
{ "id": 12, "position": [105, 80], "type": "max/plantarium/branch", "props": { "length": 3, "thickness": 0.6, "amount": 10, "rotation": 180, "offsetSingle": 0.34, "lowestBranch": 0.53, "highestBranch": 1, "depth": 1 } },
|
||||
{ "id": 13, "position": [130, 80], "type": "max/plantarium/noise", "props": { "strength": 8, "scale": 7.7, "fixBottom": 1, "directionalStrength": [1, 0, 1], "depth": 1 } },
|
||||
{ "id": 14, "position": [155, 80], "type": "max/plantarium/gravity", "props": { "strength": 0.11, "scale": 39, "fixBottom": 0, "directionalStrength": [1, 1, 1], "depth": 1, "curviness": 1 } }
|
||||
'settings': { 'resolution.circle': 26, 'resolution.curve': 39 },
|
||||
'nodes': [
|
||||
{ 'id': 9, 'position': [180, 80], 'type': 'max/plantarium/output', 'props': {} },
|
||||
{
|
||||
'id': 10,
|
||||
'position': [55, 80],
|
||||
'type': 'max/plantarium/stem',
|
||||
'props': { 'amount': 1, 'length': 11, 'thickness': 0.71 }
|
||||
},
|
||||
{
|
||||
'id': 11,
|
||||
'position': [80, 80],
|
||||
'type': 'max/plantarium/noise',
|
||||
'props': {
|
||||
'strength': 35,
|
||||
'scale': 4.6,
|
||||
'fixBottom': 1,
|
||||
'directionalStrength': [1, 0.74, 0.083],
|
||||
'depth': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 12,
|
||||
'position': [105, 80],
|
||||
'type': 'max/plantarium/branch',
|
||||
'props': {
|
||||
'length': 3,
|
||||
'thickness': 0.6,
|
||||
'amount': 10,
|
||||
'rotation': 180,
|
||||
'offsetSingle': 0.34,
|
||||
'lowestBranch': 0.53,
|
||||
'highestBranch': 1,
|
||||
'depth': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 13,
|
||||
'position': [130, 80],
|
||||
'type': 'max/plantarium/noise',
|
||||
'props': {
|
||||
'strength': 8,
|
||||
'scale': 7.7,
|
||||
'fixBottom': 1,
|
||||
'directionalStrength': [1, 0, 1],
|
||||
'depth': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 14,
|
||||
'position': [155, 80],
|
||||
'type': 'max/plantarium/gravity',
|
||||
'props': {
|
||||
'strength': 0.11,
|
||||
'scale': 39,
|
||||
'fixBottom': 0,
|
||||
'directionalStrength': [1, 1, 1],
|
||||
'depth': 1,
|
||||
'curviness': 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [[10, 0, 11, "plant"], [11, 0, 12, "plant"], [12, 0, 13, "plant"], [13, 0, 14, "plant"], [14, 0, 9, "input"]]
|
||||
}
|
||||
'edges': [
|
||||
[10, 0, 11, 'plant'],
|
||||
[11, 0, 12, 'plant'],
|
||||
[12, 0, 13, 'plant'],
|
||||
[13, 0, 14, 'plant'],
|
||||
[14, 0, 9, 'input']
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import type { Graph, Node } from "@nodarium/types";
|
||||
import type { Graph, SerializedNode } from '@nodarium/types';
|
||||
|
||||
export function tree(depth: number): Graph {
|
||||
|
||||
const nodes: Node[] = [
|
||||
const nodes: SerializedNode[] = [
|
||||
{
|
||||
id: 0,
|
||||
type: "max/plantarium/output",
|
||||
type: 'max/plantarium/output',
|
||||
position: [0, 0]
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
type: "max/plantarium/math",
|
||||
type: 'max/plantarium/math',
|
||||
position: [-40, -10]
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const edges: [number, number, number, string][] = [
|
||||
[1, 0, 0, "input"]
|
||||
[1, 0, 0, 'input']
|
||||
];
|
||||
|
||||
for (let d = 0; d < depth; d++) {
|
||||
const amount = Math.pow(2, d);
|
||||
for (let i = 0; i < amount; i++) {
|
||||
|
||||
const id0 = amount * 2 + i * 2;
|
||||
const id1 = amount * 2 + i * 2 + 1;
|
||||
|
||||
@@ -33,24 +31,22 @@ export function tree(depth: number): Graph {
|
||||
|
||||
nodes.push({
|
||||
id: id0,
|
||||
type: "max/plantarium/math",
|
||||
position: [x, y],
|
||||
type: 'max/plantarium/math',
|
||||
position: [x, y]
|
||||
});
|
||||
edges.push([id0, 0, parent, "a"]);
|
||||
edges.push([id0, 0, parent, 'a']);
|
||||
nodes.push({
|
||||
id: id1,
|
||||
type: "max/plantarium/math",
|
||||
position: [x, y + 35],
|
||||
type: 'max/plantarium/math',
|
||||
position: [x, y + 35]
|
||||
});
|
||||
edges.push([id1, 0, parent, "b"]);
|
||||
edges.push([id1, 0, parent, 'b']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
id: Math.floor(Math.random() * 100000),
|
||||
nodes,
|
||||
edges
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { getContext, type Snippet } from 'svelte';
|
||||
|
||||
let index = -1;
|
||||
let index = $state(-1);
|
||||
let wrapper: HTMLDivElement;
|
||||
|
||||
$: if (index === -1) {
|
||||
index = getContext<() => number>("registerCell")();
|
||||
}
|
||||
const { children } = $props<{ children?: Snippet }>();
|
||||
|
||||
const sizes = getContext<{ value: string[] }>("sizes");
|
||||
$effect(() => {
|
||||
if (index === -1) {
|
||||
index = getContext<() => number>('registerCell')();
|
||||
}
|
||||
});
|
||||
|
||||
const sizes = getContext<{ value: string[] }>('sizes');
|
||||
|
||||
let downSizes: string[] = [];
|
||||
let downWidth = 0;
|
||||
let mouseDown = false;
|
||||
let startX = 0;
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
downSizes = [...sizes.value];
|
||||
mouseDown = true;
|
||||
startX = event.clientX;
|
||||
downWidth = wrapper.getBoundingClientRect().width;
|
||||
@@ -31,8 +33,8 @@
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:mouseup={() => (mouseDown = false)}
|
||||
on:mousemove={handleMouseMove}
|
||||
onmouseup={() => (mouseDown = false)}
|
||||
onmousemove={handleMouseMove}
|
||||
/>
|
||||
|
||||
{#if index > 0}
|
||||
@@ -40,12 +42,13 @@
|
||||
class="seperator"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:mousedown={handleMouseDown}
|
||||
></div>
|
||||
onmousedown={handleMouseDown}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="cell" bind:this={wrapper}>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -59,7 +62,7 @@
|
||||
cursor: ew-resize;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: var(--outline);
|
||||
background: var(--color-outline);
|
||||
}
|
||||
.seperator::before {
|
||||
content: "";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from "svelte";
|
||||
import { onMount, setContext, type Snippet } from 'svelte';
|
||||
|
||||
export let id = "grid-0";
|
||||
const { children, id } = $props<{ children?: Snippet; id?: string }>();
|
||||
|
||||
setContext("grid-id", id);
|
||||
onMount(() => {
|
||||
setContext('grid-id', id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot {id} />
|
||||
{@render children({ id })}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { setContext, getContext } from "svelte";
|
||||
import { localState } from "$lib/helpers/localState.svelte";
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
const gridId = getContext<string>("grid-id") || "grid-0";
|
||||
const gridId = getContext<string>('grid-id') || 'grid-0';
|
||||
let sizes = localState<string[]>(gridId, []);
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
let registerIndex = 0;
|
||||
setContext("registerCell", function () {
|
||||
setContext('registerCell', function() {
|
||||
let index = registerIndex;
|
||||
registerIndex++;
|
||||
if (registerIndex > sizes.value.length) {
|
||||
sizes.value = [...sizes.value, "1fr"];
|
||||
sizes.value = [...sizes.value, '1fr'];
|
||||
}
|
||||
return index;
|
||||
});
|
||||
|
||||
setContext("sizes", sizes);
|
||||
setContext('sizes', sizes);
|
||||
|
||||
const cols = $derived(
|
||||
sizes.value.map((size, i) => `${i > 0 ? "1px " : ""}` + size).join(" "),
|
||||
sizes.value.map((size, i) => `${i > 0 ? '1px ' : ''}` + size).join(' ')
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withSubComponents } from "$lib/helpers";
|
||||
import Grid from "./Grid.svelte";
|
||||
import Row from "./Row.svelte";
|
||||
import Cell from "./Cell.svelte";
|
||||
import { withSubComponents } from '$lib/helpers';
|
||||
import Cell from './Cell.svelte';
|
||||
import Grid from './Grid.svelte';
|
||||
import Row from './Row.svelte';
|
||||
|
||||
export default withSubComponents(Grid, { Row, Cell });
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
|
||||
type Shortcut = {
|
||||
key: string | string[],
|
||||
shift?: boolean,
|
||||
ctrl?: boolean,
|
||||
alt?: boolean,
|
||||
preventDefault?: boolean,
|
||||
description?: string,
|
||||
callback: (event: KeyboardEvent) => void
|
||||
export type ShortCut = {
|
||||
key: string | string[];
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
alt?: boolean;
|
||||
preventDefault?: boolean;
|
||||
description?: string;
|
||||
callback: (event: KeyboardEvent) => void;
|
||||
};
|
||||
|
||||
function getShortcutId(shortcut: ShortCut) {
|
||||
return `${shortcut.key}${shortcut.shift ? '+shift' : ''}${shortcut.ctrl ? '+ctrl' : ''}${
|
||||
shortcut.alt ? '+alt' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
function getShortcutId(shortcut: Shortcut) {
|
||||
return `${shortcut.key}${shortcut.shift ? "+shift" : ""}${shortcut.ctrl ? "+ctrl" : ""}${shortcut.alt ? "+alt" : ""}`;
|
||||
}
|
||||
|
||||
export function createKeyMap(keys: Shortcut[]) {
|
||||
|
||||
export function createKeyMap(keys: ShortCut[]) {
|
||||
const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
|
||||
|
||||
return {
|
||||
handleKeyboardEvent: (event: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") return;
|
||||
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') return;
|
||||
const key = [...get(store).values()].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;
|
||||
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;
|
||||
});
|
||||
if (key && key.preventDefault) event.preventDefault();
|
||||
key?.callback(event);
|
||||
},
|
||||
addShortcut: (shortcut: Shortcut) => {
|
||||
addShortcut: (shortcut: ShortCut) => {
|
||||
if (Array.isArray(shortcut.key)) {
|
||||
for (const k of shortcut.key) {
|
||||
store.update(shortcuts => {
|
||||
@@ -52,6 +53,5 @@ export function createKeyMap(keys: Shortcut[]) {
|
||||
}
|
||||
},
|
||||
keys: derived(store, $store => Array.from($store.values()))
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function animate(duration: number, callback: (progress: number) => void |
|
||||
} else {
|
||||
callback(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
@@ -30,10 +30,11 @@ export function createNodePath({
|
||||
cornerBottom = 0,
|
||||
leftBump = false,
|
||||
rightBump = false,
|
||||
aspectRatio = 1,
|
||||
aspectRatio = 1
|
||||
} = {}) {
|
||||
return `M0,${cornerTop}
|
||||
${cornerTop
|
||||
${
|
||||
cornerTop
|
||||
? ` V${cornerTop}
|
||||
Q0,0 ${cornerTop * aspectRatio},0
|
||||
H${100 - cornerTop * aspectRatio}
|
||||
@@ -42,40 +43,45 @@ export function createNodePath({
|
||||
: ` V0
|
||||
H100
|
||||
`
|
||||
}
|
||||
}
|
||||
V${y - height / 2}
|
||||
${rightBump
|
||||
${
|
||||
rightBump
|
||||
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||
: ` H100`
|
||||
}
|
||||
${cornerBottom
|
||||
}
|
||||
${
|
||||
cornerBottom
|
||||
? ` V${100 - cornerBottom}
|
||||
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
||||
H${cornerBottom * aspectRatio}
|
||||
Q0,100 0,${100 - cornerBottom}
|
||||
`
|
||||
: `${leftBump ? `V100 H0` : `V100`}`
|
||||
}
|
||||
${leftBump
|
||||
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
||||
}
|
||||
${
|
||||
leftBump
|
||||
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
|
||||
y - height / 2
|
||||
}`
|
||||
: ` H0`
|
||||
}
|
||||
Z`.replace(/\s+/g, " ");
|
||||
}
|
||||
Z`.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
export const debounce = (fn: Function, ms = 300) => {
|
||||
export const debounce = (fn: () => void, ms = 300) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return function (this: any, ...args: any[]) {
|
||||
return function(this: unknown, ...args: unknown[]) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||
timeoutId = setTimeout(() => fn.apply(this, args as []), ms);
|
||||
};
|
||||
};
|
||||
|
||||
export const clone: <T>(v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
|
||||
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
|
||||
? globalThis.structuredClone
|
||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
|
||||
|
||||
export function withSubComponents<A, B extends Record<string, any>>(
|
||||
export function withSubComponents<A, B extends Record<string, unknown>>(
|
||||
component: A,
|
||||
subcomponents: B
|
||||
): A & B {
|
||||
@@ -87,7 +93,7 @@ export function withSubComponents<A, B extends Record<string, any>>(
|
||||
}
|
||||
|
||||
export function humanizeNumber(number: number): string {
|
||||
const suffixes = ["", "K", "M", "B", "T"];
|
||||
const suffixes = ['', 'K', 'M', 'B', 'T'];
|
||||
if (number < 1000) {
|
||||
return number.toString();
|
||||
}
|
||||
@@ -104,11 +110,15 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
||||
const millisecondsPerHour = 3600000;
|
||||
const millisecondsPerDay = 86400000;
|
||||
|
||||
let days = Math.floor(durationInMilliseconds / millisecondsPerDay);
|
||||
let hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
|
||||
let minutes = Math.floor((durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute);
|
||||
let seconds = Math.floor((durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond);
|
||||
let millis = durationInMilliseconds % millisecondsPerSecond;
|
||||
const days = Math.floor(durationInMilliseconds / millisecondsPerDay);
|
||||
const hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
|
||||
const minutes = Math.floor(
|
||||
(durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute
|
||||
);
|
||||
const seconds = Math.floor(
|
||||
(durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond
|
||||
);
|
||||
const millis = durationInMilliseconds % millisecondsPerSecond;
|
||||
|
||||
let durationString = '';
|
||||
|
||||
@@ -126,37 +136,15 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
||||
}
|
||||
|
||||
if (millis > 0 || durationString === '') {
|
||||
durationString += millis + 'ms';
|
||||
durationString += Math.floor(millis) + 'ms';
|
||||
}
|
||||
|
||||
return durationString.trim();
|
||||
}
|
||||
// export function debounceAsyncFunction<T extends any[], R>(
|
||||
// func: (...args: T) => Promise<R>
|
||||
// ): (...args: T) => Promise<R> {
|
||||
// let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
// let lastPromise: Promise<R> | null = null;
|
||||
// let lastReject: ((reason?: any) => void) | null = null;
|
||||
//
|
||||
// return (...args: T): Promise<R> => {
|
||||
// if (timeoutId) {
|
||||
// clearTimeout(timeoutId);
|
||||
// if (lastReject) {
|
||||
// lastReject(new Error("Debounced: Previous call was canceled."));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new Promise<R>((resolve, reject) => {
|
||||
// lastReject = reject;
|
||||
// timeoutId = setTimeout(() => {
|
||||
// timeoutId = null;
|
||||
// lastReject = null;
|
||||
// lastPromise = func(...args).then(resolve, reject);
|
||||
// }, 300); // Default debounce time is 300ms; you can make this configurable.
|
||||
// });
|
||||
// };
|
||||
// }
|
||||
export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>>(asyncFn: T): T {
|
||||
|
||||
export function debounceAsyncFunction<T extends (...args: never[]) => Promise<unknown>>(
|
||||
asyncFn: T
|
||||
): T {
|
||||
let isRunning = false;
|
||||
let latestArgs: Parameters<T> | null = null;
|
||||
let resolveNext: (() => void) | null = null;
|
||||
@@ -177,7 +165,7 @@ export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>
|
||||
try {
|
||||
// Execute with the latest arguments
|
||||
const result = await asyncFn(...latestArgs!);
|
||||
return result;
|
||||
return result as ReturnType<T>;
|
||||
} finally {
|
||||
// Allow the next execution
|
||||
isRunning = false;
|
||||
@@ -190,48 +178,18 @@ export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>
|
||||
}) as T;
|
||||
}
|
||||
|
||||
// export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
|
||||
// let currentPromise: Promise<R> | null = null;
|
||||
// let nextArgs: T | null = null;
|
||||
// let resolveNext: ((result: R) => void) | null = null;
|
||||
//
|
||||
// const debouncedFunction = async (...args: T): Promise<R> => {
|
||||
// if (currentPromise) {
|
||||
// // Store the latest arguments and create a new promise to resolve them later
|
||||
// nextArgs = args;
|
||||
// return new Promise<R>((resolve) => {
|
||||
// resolveNext = resolve;
|
||||
// });
|
||||
// } else {
|
||||
// // Execute the function immediately
|
||||
// try {
|
||||
// currentPromise = func(...args);
|
||||
// const result = await currentPromise;
|
||||
// return result;
|
||||
// } finally {
|
||||
// currentPromise = null;
|
||||
// // If there are stored arguments, call the function again with the latest arguments
|
||||
// if (nextArgs) {
|
||||
// const argsToUse = nextArgs;
|
||||
// const resolver = resolveNext;
|
||||
// nextArgs = null;
|
||||
// resolveNext = null;
|
||||
// resolver!(await debouncedFunction(...argsToUse));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// return debouncedFunction;
|
||||
// }
|
||||
|
||||
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
|
||||
export function withArgsChangeOnly<T extends unknown[], R>(
|
||||
func: (...args: T) => R
|
||||
): (...args: T) => R {
|
||||
let lastArgs: T | undefined = undefined;
|
||||
let lastResult: R;
|
||||
|
||||
return (...args: T): R => {
|
||||
// Check if arguments are the same as last call
|
||||
if (lastArgs && args.length === lastArgs.length && args.every((val, index) => val === lastArgs?.[index])) {
|
||||
if (
|
||||
lastArgs && args.length === lastArgs.length
|
||||
&& args.every((val, index) => val === lastArgs?.[index])
|
||||
) {
|
||||
return lastResult; // Return cached result if arguments haven't changed
|
||||
}
|
||||
|
||||
@@ -241,4 +199,3 @@ export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R):
|
||||
return lastResult; // Return new result
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export class LocalStore<T> {
|
||||
value = $state<T>() as T;
|
||||
key = "";
|
||||
key = '';
|
||||
|
||||
constructor(key: string, value: T) {
|
||||
this.key = key;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { type Writable, writable } from 'svelte/store';
|
||||
|
||||
function isStore(v: unknown): v is Writable<unknown> {
|
||||
return v !== null && typeof v === "object" && "subscribe" in v && "set" in v;
|
||||
return v !== null && typeof v === 'object' && 'subscribe' in v && 'set' in v;
|
||||
}
|
||||
|
||||
const storeIds: Map<string, ReturnType<typeof createLocalStore>> = new Map();
|
||||
|
||||
const HAS_LOCALSTORAGE = "localStorage" in globalThis;
|
||||
const HAS_LOCALSTORAGE = 'localStorage' in globalThis;
|
||||
|
||||
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||
|
||||
let store: Writable<T>;
|
||||
|
||||
if (HAS_LOCALSTORAGE) {
|
||||
@@ -36,18 +35,15 @@ function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||
subscribe: store.subscribe,
|
||||
set: store.set,
|
||||
update: store.update
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
||||
|
||||
if (storeIds.has(key)) return storeIds.get(key) as Writable<T>;
|
||||
|
||||
const store = createLocalStore(key, initialValue)
|
||||
const store = createLocalStore(key, initialValue);
|
||||
|
||||
storeIds.set(key, store);
|
||||
|
||||
return store
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default <T extends unknown[]>(
|
||||
callback: (...args: T) => void,
|
||||
delay: number,
|
||||
delay: number
|
||||
) => {
|
||||
let isWaiting = false;
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17" y="8" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
|
||||
<rect x="2" y="3" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
|
||||
<rect x="2" y="14" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M16 10.5C9.33333 10.5 14.8889 6 8.22222 6H6M16 12.5C8.77778 12.5 14.8889 17 8.22222 17H6" stroke="currentColor" stroke-width="2"/>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17" y="8" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2" />
|
||||
<rect x="2" y="3" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2" />
|
||||
<rect x="2" y="14" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2" />
|
||||
<path
|
||||
d="M16 10.5C9.33333 10.5 14.8889 6 8.22222 6H6M16 12.5C8.77778 12.5 14.8889 17 8.22222 17H6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 519 B After Width: | Height: | Size: 509 B |
@@ -1,13 +1,14 @@
|
||||
import { createWasmWrapper } from "@nodarium/utils";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { createWasmWrapper } from '@nodarium/utils';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export async function getWasm(id: `${string}/${string}/${string}`) {
|
||||
const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`);
|
||||
const filePath = path.resolve(`./static/nodes/${id}`);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch (e) {
|
||||
console.error(`Failed to read node: ${id}`, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,9 +21,11 @@ export async function getNodeWasm(id: `${string}/${string}/${string}`) {
|
||||
const wasmBytes = await getWasm(id);
|
||||
if (!wasmBytes) return null;
|
||||
|
||||
const wrapper = createWasmWrapper(wasmBytes);
|
||||
|
||||
return wrapper;
|
||||
try {
|
||||
return createWasmWrapper(wasmBytes.buffer);
|
||||
} catch (error) {
|
||||
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNode(id: `${string}/${string}/${string}`) {
|
||||
@@ -36,12 +39,12 @@ export async function getNode(id: `${string}/${string}/${string}`) {
|
||||
}
|
||||
|
||||
export async function getCollectionNodes(userId: `${string}/${string}`) {
|
||||
const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`));
|
||||
const nodes = await fs.readdir(path.resolve(`./static/nodes/${userId}`));
|
||||
return nodes
|
||||
.filter((n) => n !== "pkg" && n !== ".template")
|
||||
.filter((n) => n !== 'pkg' && n !== '.template')
|
||||
.map((n) => {
|
||||
return {
|
||||
id: `${userId}/${n}`,
|
||||
id: `${userId}/${n}`
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -50,20 +53,20 @@ export async function getCollection(userId: `${string}/${string}`) {
|
||||
const nodes = await getCollectionNodes(userId);
|
||||
return {
|
||||
id: userId,
|
||||
nodes,
|
||||
nodes
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserCollections(userId: string) {
|
||||
const collections = await fs.readdir(path.resolve(`../nodes/${userId}`));
|
||||
const collections = await fs.readdir(path.resolve(`./static/nodes/${userId}`));
|
||||
return Promise.all(
|
||||
collections.map(async (n) => {
|
||||
const nodes = await getCollectionNodes(`${userId}/${n}`);
|
||||
return {
|
||||
id: `${userId}/${n}`,
|
||||
nodes,
|
||||
nodes
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,20 +74,20 @@ export async function getUser(userId: string) {
|
||||
const collections = await getUserCollections(userId);
|
||||
return {
|
||||
id: userId,
|
||||
collections,
|
||||
collections
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUsers() {
|
||||
const nodes = await fs.readdir(path.resolve("../nodes"));
|
||||
const nodes = await fs.readdir(path.resolve('./static/nodes'));
|
||||
const users = await Promise.all(
|
||||
nodes.map(async (n) => {
|
||||
const collections = await getUserCollections(n);
|
||||
return {
|
||||
id: n,
|
||||
collections,
|
||||
collections
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
return users;
|
||||
}
|
||||
|
||||
2
app/src/lib/node-registry/index.ts
Normal file
2
app/src/lib/node-registry/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './node-registry-cache';
|
||||
export * from './node-registry-client';
|
||||
56
app/src/lib/node-registry/node-registry-cache.ts
Normal file
56
app/src/lib/node-registry/node-registry-cache.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { AsyncCache } from '@nodarium/types';
|
||||
import { type IDBPDatabase, openDB } from 'idb';
|
||||
|
||||
export class IndexDBCache implements AsyncCache<unknown> {
|
||||
size: number = 100;
|
||||
|
||||
db: Promise<IDBPDatabase<unknown>>;
|
||||
private _cache = new Map<string, unknown>();
|
||||
|
||||
constructor(id: string) {
|
||||
this.db = openDB<unknown>('cache/' + id, 1, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore('keyval');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T> {
|
||||
let res = this._cache.get(key);
|
||||
if (!res) {
|
||||
res = await (await this.db).get('keyval', key);
|
||||
}
|
||||
if (res) {
|
||||
this._cache.set(key, res);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
|
||||
async getArrayBuffer(key: string) {
|
||||
const res = await this.get(key);
|
||||
if (!res) return;
|
||||
if (res instanceof ArrayBuffer) {
|
||||
return res;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async getString(key: string) {
|
||||
const res = await this.get(key);
|
||||
if (!res) return;
|
||||
if (typeof res === 'string') {
|
||||
return res;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async set(key: string, value: unknown) {
|
||||
this._cache.set(key, value);
|
||||
const db = await this.db;
|
||||
await db.put('keyval', value, key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.db.then(db => db.clear('keyval'));
|
||||
}
|
||||
}
|
||||
168
app/src/lib/node-registry/node-registry-client.ts
Normal file
168
app/src/lib/node-registry/node-registry-client.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
type AsyncCache,
|
||||
type NodeDefinition,
|
||||
NodeDefinitionSchema,
|
||||
type NodeRegistry
|
||||
} from '@nodarium/types';
|
||||
import { createLogger, createWasmWrapper } from '@nodarium/utils';
|
||||
|
||||
const log = createLogger('node-registry');
|
||||
log.mute();
|
||||
|
||||
export class RemoteNodeRegistry implements NodeRegistry {
|
||||
status: 'loading' | 'ready' | 'error' = 'loading';
|
||||
private nodes: Map<string, NodeDefinition> = new Map();
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
public cache?: AsyncCache<ArrayBuffer | string>
|
||||
) {}
|
||||
|
||||
async fetchJson(url: string, skipCache = false) {
|
||||
const finalUrl = `${this.url}/${url}`;
|
||||
|
||||
if (!skipCache && this.cache) {
|
||||
const cachedValue = await this.cache?.get<string>(finalUrl);
|
||||
if (cachedValue) {
|
||||
// fetch again in the background, maybe implement that only refetch after a certain time
|
||||
this.fetchJson(url, true);
|
||||
return JSON.parse(cachedValue);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(finalUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
log.error(`Failed to load ${url}`, { response, url, host: this.url });
|
||||
throw new Error(`Failed to load ${url}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
this.cache?.set(finalUrl, JSON.stringify(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async fetchArrayBuffer(url: string, skipCache = false) {
|
||||
const finalUrl = `${this.url}/${url}`;
|
||||
|
||||
if (!skipCache && this.cache) {
|
||||
const cachedNode = await this.cache?.get<ArrayBuffer>(finalUrl);
|
||||
if (cachedNode) {
|
||||
// fetch again in the background, maybe implement that only refetch after a certain time
|
||||
this.fetchArrayBuffer(url, true);
|
||||
return cachedNode;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(finalUrl);
|
||||
if (!response.ok) {
|
||||
log.error(`Failed to load ${url}`, { response, url, host: this.url });
|
||||
throw new Error(`Failed to load ${url}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
this.cache?.set(finalUrl, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async fetchUsers() {
|
||||
return this.fetchJson(`nodes/users.json`);
|
||||
}
|
||||
|
||||
async fetchUser(userId: `${string}`) {
|
||||
return this.fetchJson(`user/${userId}.json`);
|
||||
}
|
||||
|
||||
async fetchCollection(userCollectionId: `${string}/${string}`) {
|
||||
const col = await this.fetchJson(`nodes/${userCollectionId}.json`);
|
||||
return col;
|
||||
}
|
||||
|
||||
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
|
||||
return this.fetchJson(`nodes/${nodeId}.json`);
|
||||
}
|
||||
|
||||
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
|
||||
const node = await this.fetchArrayBuffer(`nodes/${nodeId}.wasm`);
|
||||
if (!node) {
|
||||
throw new Error(`Failed to load node wasm ${nodeId}`);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
async load(nodeIds: `${string}/${string}/${string}`[]) {
|
||||
const a = performance.now();
|
||||
|
||||
const nodes = (await Promise.all(
|
||||
[...new Set(nodeIds).values()].map(async (id) => {
|
||||
if (this.nodes.has(id)) {
|
||||
return this.nodes.get(id)!;
|
||||
}
|
||||
|
||||
const wasmBuffer = await this.fetchNodeWasm(id);
|
||||
|
||||
try {
|
||||
return await this.register(id, wasmBuffer);
|
||||
} catch (e) {
|
||||
console.log('Failed to register: ', id);
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
})
|
||||
)).filter(Boolean) as NodeDefinition[];
|
||||
|
||||
const duration = performance.now() - a;
|
||||
|
||||
log.group('loaded nodes in', duration, 'ms');
|
||||
log.info(nodeIds);
|
||||
log.info(nodes);
|
||||
log.groupEnd();
|
||||
this.status = 'ready';
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async register(id: string, wasmBuffer: ArrayBuffer) {
|
||||
let wrapper: ReturnType<typeof createWasmWrapper> = null!;
|
||||
try {
|
||||
wrapper = createWasmWrapper(wasmBuffer);
|
||||
} catch (error) {
|
||||
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
||||
}
|
||||
|
||||
const rawDefinition = wrapper.get_definition();
|
||||
const definition = NodeDefinitionSchema.safeParse(rawDefinition);
|
||||
|
||||
if (definition.error) {
|
||||
throw new Error(
|
||||
'Failed to parse node definition from node:+\n' + JSON.stringify(rawDefinition, null, 2)
|
||||
+ '\n'
|
||||
+ definition.error
|
||||
);
|
||||
}
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(definition.data.id, wasmBuffer);
|
||||
}
|
||||
|
||||
const node = {
|
||||
...definition.data,
|
||||
execute: wrapper.execute
|
||||
};
|
||||
|
||||
this.nodes.set(definition.data.id, node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
getNode(id: string) {
|
||||
return this.nodes.get(id);
|
||||
}
|
||||
|
||||
getAllNodes() {
|
||||
return [...this.nodes.values()];
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Select } from "@nodarium/ui";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { InputSelect } from '@nodarium/ui';
|
||||
|
||||
let activeStore = 0;
|
||||
export let activeId: Writable<string>;
|
||||
$: [activeUser, activeCollection, activeNode] = $activeId.split(`/`);
|
||||
let activeStore = $state(0);
|
||||
let { activeId }: { activeId: string } = $props();
|
||||
const [activeUser, activeCollection, activeNode] = $derived(
|
||||
activeId.split(`/`)
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="breadcrumbs">
|
||||
{#if activeUser}
|
||||
<Select id="root" options={["root"]} bind:value={activeStore}></Select>
|
||||
<InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
|
||||
{#if activeCollection}
|
||||
<button
|
||||
on:click={() => {
|
||||
$activeId = activeUser;
|
||||
onclick={() => {
|
||||
activeId = activeUser;
|
||||
}}
|
||||
>
|
||||
{activeUser}
|
||||
</button>
|
||||
{#if activeNode}
|
||||
<button
|
||||
on:click={() => {
|
||||
$activeId = `${activeUser}/${activeCollection}`;
|
||||
onclick={() => {
|
||||
activeId = `${activeUser}/${activeCollection}`;
|
||||
}}
|
||||
>
|
||||
{activeCollection}
|
||||
@@ -34,7 +35,7 @@
|
||||
<span>{activeUser}</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<Select id="root" options={["root"]} bind:value={activeStore}></Select>
|
||||
<InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -46,7 +47,7 @@
|
||||
gap: 0.8em;
|
||||
height: 35px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
}
|
||||
.breadcrumbs > button {
|
||||
position: relative;
|
||||
|
||||
@@ -1,52 +1,59 @@
|
||||
<script lang="ts">
|
||||
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
|
||||
import type { NodeDefinition } from "@nodarium/types";
|
||||
import NodeHtml from '$lib/graph-interface/node/NodeHTML.svelte';
|
||||
import type { NodeDefinition, NodeId, NodeInstance } from '@nodarium/types';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let node: NodeDefinition;
|
||||
const { node }: { node: NodeDefinition } = $props();
|
||||
|
||||
let dragging = false;
|
||||
let dragging = $state(false);
|
||||
|
||||
let nodeData = {
|
||||
id: 0,
|
||||
type: node?.id,
|
||||
position: [0, 0] as [number, number],
|
||||
props: {},
|
||||
tmp: {
|
||||
type: node,
|
||||
},
|
||||
};
|
||||
let nodeData = $state<NodeInstance>(null!);
|
||||
|
||||
function handleDragStart(e: DragEvent) {
|
||||
dragging = true;
|
||||
const box = (e?.target as HTMLElement)?.getBoundingClientRect();
|
||||
if (e.dataTransfer === null) return;
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("data/node-id", node.id);
|
||||
if (nodeData.props) {
|
||||
e.dataTransfer.setData("data/node-props", JSON.stringify(nodeData.props));
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('data/node-id', node.id.toString());
|
||||
if (nodeData?.props) {
|
||||
e.dataTransfer.setData('data/node-props', JSON.stringify(nodeData.props));
|
||||
}
|
||||
e.dataTransfer.setData(
|
||||
"data/node-offset-x",
|
||||
Math.round(box.left - e.clientX).toString(),
|
||||
'data/node-offset-x',
|
||||
Math.round(box.left - e.clientX).toString()
|
||||
);
|
||||
e.dataTransfer.setData(
|
||||
"data/node-offset-y",
|
||||
Math.round(box.top - e.clientY).toString(),
|
||||
'data/node-offset-y',
|
||||
Math.round(box.top - e.clientY).toString()
|
||||
);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
nodeData = {
|
||||
id: 0,
|
||||
type: node.id as unknown as NodeId,
|
||||
position: [0, 0] as [number, number],
|
||||
props: {},
|
||||
state: {
|
||||
type: node
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="node-wrapper" class:dragging>
|
||||
<div
|
||||
on:dragend={() => {
|
||||
ondragend={() => {
|
||||
dragging = false;
|
||||
}}
|
||||
draggable={true}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:dragstart={handleDragStart}
|
||||
ondragstart={handleDragStart}
|
||||
>
|
||||
<NodeHtml inView={true} position={"relative"} z={5} bind:node={nodeData} />
|
||||
{#if nodeData}
|
||||
<NodeHtml bind:node={nodeData} inView={true} position="relative" z={5} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +68,7 @@
|
||||
}
|
||||
|
||||
.dragging {
|
||||
border: dashed 2px var(--outline);
|
||||
border: dashed 2px var(--color-outline);
|
||||
}
|
||||
.node-wrapper > div {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import BreadCrumbs from "./BreadCrumbs.svelte";
|
||||
import DraggableNode from "./DraggableNode.svelte";
|
||||
import type { RemoteNodeRegistry } from "@nodarium/registry";
|
||||
import type { RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||
import BreadCrumbs from './BreadCrumbs.svelte';
|
||||
import DraggableNode from './DraggableNode.svelte';
|
||||
|
||||
export let registry: RemoteNodeRegistry;
|
||||
const { registry }: { registry: RemoteNodeRegistry } = $props();
|
||||
|
||||
const activeId = writable("max/plantarium");
|
||||
let activeId = $state('max/plantarium');
|
||||
let showBreadCrumbs = false;
|
||||
|
||||
// const activeId = localStore<
|
||||
// `${string}` | `${string}/${string}` | `${string}/${string}/${string}`
|
||||
// >("nodes.store.activeId", "");
|
||||
|
||||
$: [activeUser, activeCollection, activeNode] = $activeId.split(`/`);
|
||||
const [activeUser, activeCollection, activeNode] = $derived(
|
||||
activeId.split(`/`)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if showBreadCrumbs}
|
||||
@@ -25,12 +22,14 @@
|
||||
{#await registry.fetchUsers()}
|
||||
<div>Loading Users...</div>
|
||||
{:then users}
|
||||
{#each users as user}
|
||||
{#each users as user (user.id)}
|
||||
<button
|
||||
on:click={() => {
|
||||
$activeId = user.id;
|
||||
}}>{user.id}</button
|
||||
onclick={() => {
|
||||
activeId = user.id;
|
||||
}}
|
||||
>
|
||||
{user.id}
|
||||
</button>
|
||||
{/each}
|
||||
{:catch error}
|
||||
<div>{error.message}</div>
|
||||
@@ -39,10 +38,10 @@
|
||||
{#await registry.fetchUser(activeUser)}
|
||||
<div>Loading User...</div>
|
||||
{:then user}
|
||||
{#each user.collections as collection}
|
||||
{#each user.collections as collection (collection)}
|
||||
<button
|
||||
on:click={() => {
|
||||
$activeId = collection.id;
|
||||
onclick={() => {
|
||||
activeId = collection.id;
|
||||
}}
|
||||
>
|
||||
{collection.id.split(`/`)[1]}
|
||||
@@ -55,7 +54,7 @@
|
||||
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
|
||||
<div>Loading Collection...</div>
|
||||
{:then collection}
|
||||
{#each collection.nodes as node}
|
||||
{#each collection.nodes as node (node.id)}
|
||||
{#await registry.fetchNodeDefinition(node.id)}
|
||||
<div>Loading Node... {node.id}</div>
|
||||
{:then node}
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
<script lang="ts">
|
||||
export let labels: string[] = [];
|
||||
export let values: number[] = [];
|
||||
type Props = {
|
||||
labels: string[];
|
||||
values: number[];
|
||||
};
|
||||
|
||||
$: total = values.reduce((acc, v) => acc + v, 0);
|
||||
const { labels, values }: Props = $props();
|
||||
|
||||
let colors = ["red", "green", "blue"];
|
||||
const total = $derived(values.reduce((acc, v) => acc + v, 0));
|
||||
|
||||
let colors = ['red', 'green', 'blue'];
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="bars">
|
||||
{#each values as value, i}
|
||||
<div class="bar bg-{colors[i]}" style="width: {(value / total) * 100}%;">
|
||||
{#each values as value, i (value)}
|
||||
<div
|
||||
class="bar bg-{colors[i]}-400"
|
||||
style:width={(value / total) * 100 + '%'}
|
||||
>
|
||||
{Math.round(value)}ms
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="labels mt-2">
|
||||
{#each values as _label, i}
|
||||
<div class="text-{colors[i]}">{labels[i]}</div>
|
||||
{#each values as _label, i (_label)}
|
||||
<div class="text-{colors[i]}-400">{labels[i]}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="bg-red bg-green bg-yellow bg-blue text-red text-green text-yellow text-blue"
|
||||
class="bg-red-400 bg-green-400 bg-blue-400 text-red-400 text-green-400 text-blue-400"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,52 +1,58 @@
|
||||
<script lang="ts">
|
||||
export let points: number[];
|
||||
type Props = {
|
||||
points: number[];
|
||||
type?: string;
|
||||
title?: string;
|
||||
max?: number;
|
||||
min?: number;
|
||||
};
|
||||
|
||||
export let type = "ms";
|
||||
export let title = "Performance";
|
||||
export let max: number | undefined = undefined;
|
||||
export let min: number | undefined = undefined;
|
||||
let {
|
||||
points,
|
||||
type = 'ms',
|
||||
title = 'Performance',
|
||||
max,
|
||||
min
|
||||
}: Props = $props();
|
||||
|
||||
function getMax(m?: number) {
|
||||
if (type === "%") {
|
||||
let internalMax = $derived(max ?? Math.max(...points));
|
||||
let internalMin = $derived(min ?? Math.min(...points))!;
|
||||
|
||||
const maxText = $derived.by(() => {
|
||||
if (type === '%') {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (m !== undefined) {
|
||||
if (m < 1) {
|
||||
return Math.floor(m * 100) / 100;
|
||||
if (internalMax !== undefined) {
|
||||
if (internalMax < 1) {
|
||||
return Math.floor(internalMax * 100) / 100;
|
||||
}
|
||||
if (m < 10) {
|
||||
return Math.floor(m * 10) / 10;
|
||||
if (internalMax < 10) {
|
||||
return Math.floor(internalMax * 10) / 10;
|
||||
}
|
||||
return Math.floor(m);
|
||||
return Math.floor(internalMax);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
function constructPath() {
|
||||
max = max !== undefined ? max : Math.max(...points);
|
||||
min = min !== undefined ? min : Math.min(...points);
|
||||
const mi = min as number;
|
||||
const ma = max as number;
|
||||
return points
|
||||
const path = $derived(
|
||||
points
|
||||
.map((point, i) => {
|
||||
const x = (i / (points.length - 1)) * 100;
|
||||
const y = 100 - ((point - mi) / (ma - mi)) * 100;
|
||||
const y = 100 - ((point - internalMin) / (internalMax - internalMin)) * 100;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
.join(' ')
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<p>{title}</p>
|
||||
<span class="min">{Math.floor(min || 0)}{type}</span>
|
||||
<span class="max">{getMax(max)}{type}</span>
|
||||
<span class="min">{Math.floor(internalMin || 0)}{type}</span>
|
||||
<span class="max">{maxText}{type}</span>
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 100 100">
|
||||
{#key points}
|
||||
<polyline vector-effect="non-scaling-stroke" points={constructPath()} />
|
||||
{/key}
|
||||
<polyline vector-effect="non-scaling-stroke" points={path} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +74,7 @@
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
display: flex;
|
||||
}
|
||||
p {
|
||||
@@ -82,13 +88,13 @@
|
||||
svg {
|
||||
height: 124px;
|
||||
margin: 24px 0px;
|
||||
border-top: solid thin var(--outline);
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-top: solid thin var(--color-outline);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
width: 100%;
|
||||
}
|
||||
polyline {
|
||||
fill: none;
|
||||
stroke: var(--layer-3);
|
||||
stroke: var(--color-layer-3);
|
||||
opacity: 0.5;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
<script lang="ts">
|
||||
import Monitor from "./Monitor.svelte";
|
||||
import { humanizeNumber } from "$lib/helpers";
|
||||
import { Checkbox } from "@nodarium/ui";
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import { type PerformanceData } from "@nodarium/utils";
|
||||
import BarSplit from "./BarSplit.svelte";
|
||||
import { humanizeNumber } from '$lib/helpers';
|
||||
import { InputCheckbox } from '@nodarium/ui';
|
||||
import type { PerformanceData } from '@nodarium/utils';
|
||||
import BarSplit from './BarSplit.svelte';
|
||||
import Monitor from './Monitor.svelte';
|
||||
|
||||
export let data: PerformanceData;
|
||||
const { data }: { data: PerformanceData } = $props();
|
||||
|
||||
let activeType = localStore<string>("nodes.performance.active-type", "total");
|
||||
let showAverage = true;
|
||||
|
||||
function getAverage(key: string) {
|
||||
return (
|
||||
data
|
||||
.map((run) => run[key]?.[0])
|
||||
.filter((v) => v !== undefined)
|
||||
.reduce((acc, run) => acc + run, 0) / data.length
|
||||
);
|
||||
}
|
||||
let activeType = $state('total');
|
||||
let showAverage = $state(true);
|
||||
|
||||
function round(v: number) {
|
||||
if (v < 1) {
|
||||
@@ -30,111 +20,136 @@
|
||||
return Math.floor(v);
|
||||
}
|
||||
|
||||
function getAverages() {
|
||||
let lastRun = data.at(-1);
|
||||
if (!lastRun) return {};
|
||||
return Object.keys(lastRun).reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = getAverage(key);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
function getTitle(t: string) {
|
||||
if (t.includes('/')) {
|
||||
return `Node ${t.split('/').slice(-1).join('/')}`;
|
||||
}
|
||||
|
||||
return t
|
||||
.split('-')
|
||||
.map((v) => v[0].toUpperCase() + v.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
const viewerKeys = [
|
||||
'total-vertices',
|
||||
'total-faces',
|
||||
'update-geometries',
|
||||
'split-result'
|
||||
];
|
||||
|
||||
// --- Small helpers that query `data` directly ---
|
||||
function getAverage(key: string) {
|
||||
const vals = data
|
||||
.map((run) => run[key]?.[0])
|
||||
.filter((v) => v !== undefined) as number[];
|
||||
|
||||
if (vals.length === 0) return 0;
|
||||
return vals.reduce((acc, v) => acc + v, 0) / vals.length;
|
||||
}
|
||||
|
||||
function getLast(key: string) {
|
||||
return data.at(-1)?.[key]?.[0] || 0;
|
||||
}
|
||||
|
||||
function getLasts() {
|
||||
return data.at(-1) || {};
|
||||
}
|
||||
const averages = $derived.by(() => {
|
||||
const lr = data.at(-1);
|
||||
if (!lr) return {} as Record<string, number>;
|
||||
return Object.keys(lr).reduce((acc: Record<string, number>, key) => {
|
||||
acc[key] = getAverage(key);
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
function getTotalPerformance(onlyLast = false) {
|
||||
if (onlyLast) {
|
||||
return (
|
||||
getLast("runtime") +
|
||||
getLast("update-geometries") +
|
||||
getLast("worker-transfer")
|
||||
);
|
||||
const lasts = $derived.by(() => data.at(-1) || {});
|
||||
|
||||
const totalPerformance = $derived.by(() => {
|
||||
const onlyLast = getLast('runtime')
|
||||
+ getLast('update-geometries')
|
||||
+ getLast('worker-transfer');
|
||||
const average = getAverage('runtime')
|
||||
+ getAverage('update-geometries')
|
||||
+ getAverage('worker-transfer');
|
||||
return { onlyLast, average };
|
||||
});
|
||||
|
||||
const cacheRatio = $derived.by(() => {
|
||||
return {
|
||||
onlyLast: Math.floor(getLast('cache-hit') * 100),
|
||||
average: Math.floor(getAverage('cache-hit') * 100)
|
||||
};
|
||||
});
|
||||
|
||||
const performanceData = $derived.by(() => {
|
||||
const source = showAverage ? averages : lasts;
|
||||
return Object.entries(source)
|
||||
.filter(
|
||||
([key]) =>
|
||||
!key.startsWith('node/')
|
||||
&& key !== 'total'
|
||||
&& !key.includes('cache')
|
||||
&& !viewerKeys.includes(key)
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
});
|
||||
|
||||
const nodePerformanceData = $derived.by(() => {
|
||||
const source = showAverage ? averages : lasts;
|
||||
return Object.entries(source)
|
||||
.filter(([key]) => key.startsWith('node/'))
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
});
|
||||
|
||||
const viewerPerformanceData = $derived.by(() => {
|
||||
const source = showAverage ? averages : lasts;
|
||||
return Object.entries(source)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key !== 'total-vertices'
|
||||
&& key !== 'total-faces'
|
||||
&& viewerKeys.includes(key)
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
});
|
||||
|
||||
const splitValues = $derived.by(() => {
|
||||
if (showAverage) {
|
||||
return [
|
||||
getAverage('worker-transfer'),
|
||||
getAverage('runtime'),
|
||||
getAverage('update-geometries')
|
||||
];
|
||||
}
|
||||
return (
|
||||
getAverage("runtime") +
|
||||
getAverage("update-geometries") +
|
||||
getAverage("worker-transfer")
|
||||
);
|
||||
}
|
||||
return [
|
||||
getLast('worker-transfer'),
|
||||
getLast('runtime'),
|
||||
getLast('update-geometries')
|
||||
];
|
||||
});
|
||||
|
||||
function getCacheRatio(onlyLast = false) {
|
||||
let ratio = onlyLast ? getLast("cache-hit") : getAverage("cache-hit");
|
||||
|
||||
return Math.floor(ratio * 100);
|
||||
}
|
||||
|
||||
const viewerKeys = [
|
||||
"total-vertices",
|
||||
"total-faces",
|
||||
"update-geometries",
|
||||
"split-result",
|
||||
];
|
||||
|
||||
function getPerformanceData(onlyLast: boolean = false) {
|
||||
let data = onlyLast ? getLasts() : getAverages();
|
||||
|
||||
return Object.entries(data)
|
||||
.filter(
|
||||
([key]) =>
|
||||
!key.startsWith("node/") &&
|
||||
key !== "total" &&
|
||||
!key.includes("cache") &&
|
||||
!viewerKeys.includes(key),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
function getNodePerformanceData(onlyLast: boolean = false) {
|
||||
let data = onlyLast ? getLasts() : getAverages();
|
||||
|
||||
return Object.entries(data)
|
||||
.filter(([key]) => key.startsWith("node/"))
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
function getViewerPerformanceData(onlyLast: boolean = false) {
|
||||
let data = onlyLast ? getLasts() : getAverages();
|
||||
return Object.entries(data)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key !== "total-vertices" &&
|
||||
key !== "total-faces" &&
|
||||
viewerKeys.includes(key),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
function getTotalPoints() {
|
||||
const totalPoints = $derived.by(() => {
|
||||
if (showAverage) {
|
||||
return data.map((run) => {
|
||||
return (
|
||||
run["runtime"].reduce((acc, v) => acc + v, 0) +
|
||||
run["update-geometries"].reduce((acc, v) => acc + v, 0) +
|
||||
(run["worker-transfer"]?.reduce((acc, v) => acc + v, 0) || 0)
|
||||
(run['runtime']?.reduce((acc, v) => acc + v, 0) || 0)
|
||||
+ (run['update-geometries']?.reduce((acc, v) => acc + v, 0) || 0)
|
||||
+ (run['worker-transfer']?.reduce((acc, v) => acc + v, 0) || 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return data.map((run) => {
|
||||
return (
|
||||
run["runtime"][0] +
|
||||
run["update-geometries"][0] +
|
||||
(run["worker-transfer"]?.[0] || 0)
|
||||
(run['runtime']?.[0] || 0)
|
||||
+ (run['update-geometries']?.[0] || 0)
|
||||
+ (run['worker-transfer']?.[0] || 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function constructPoints(key: string) {
|
||||
if (key === "total") {
|
||||
return getTotalPoints();
|
||||
if (key === 'total') {
|
||||
return totalPoints;
|
||||
}
|
||||
return data.map((run) => {
|
||||
if (key in run) {
|
||||
@@ -148,166 +163,140 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getSplitValues(): number[] {
|
||||
if (showAverage) {
|
||||
return [
|
||||
getAverage("worker-transfer"),
|
||||
getAverage("runtime"),
|
||||
getAverage("update-geometries"),
|
||||
];
|
||||
}
|
||||
const computedTotalDisplay = $derived.by(() =>
|
||||
round(showAverage ? totalPerformance.average : totalPerformance.onlyLast)
|
||||
);
|
||||
|
||||
return [
|
||||
getLast("worker-transfer"),
|
||||
getLast("runtime"),
|
||||
getLast("update-geometries"),
|
||||
];
|
||||
}
|
||||
|
||||
function getTitle(t: string) {
|
||||
if (t.includes("/")) {
|
||||
return `Node ${t.split("/").slice(-1).join("/")}`;
|
||||
}
|
||||
|
||||
return t
|
||||
.split("-")
|
||||
.map((v) => v[0].toUpperCase() + v.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
const computedFps = $derived.by(() =>
|
||||
Math.floor(
|
||||
1000
|
||||
/ (showAverage
|
||||
? totalPerformance.average || 1
|
||||
: totalPerformance.onlyLast || 1)
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#key $activeType && data}
|
||||
{#if $activeType === "cache-hit"}
|
||||
{#if data.length !== 0}
|
||||
{#if activeType === 'cache-hit'}
|
||||
<Monitor
|
||||
title="Cache Hits"
|
||||
points={constructPoints($activeType)}
|
||||
points={constructPoints(activeType)}
|
||||
min={0}
|
||||
max={1}
|
||||
type="%"
|
||||
/>
|
||||
{:else}
|
||||
<Monitor
|
||||
title={getTitle($activeType)}
|
||||
points={constructPoints($activeType)}
|
||||
title={getTitle(activeType)}
|
||||
points={constructPoints(activeType)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="p-4 performance-tabler">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox id="show-total" bind:value={showAverage} />
|
||||
<InputCheckbox id="show-total" bind:value={showAverage} />
|
||||
<label for="show-total">Show Average</label>
|
||||
</div>
|
||||
|
||||
{#if data.length !== 0}
|
||||
<BarSplit
|
||||
labels={["worker-transfer", "runtime", "update-geometries"]}
|
||||
values={getSplitValues()}
|
||||
/>
|
||||
<BarSplit
|
||||
labels={['worker-transfer', 'runtime', 'update-geometries']}
|
||||
values={splitValues}
|
||||
/>
|
||||
|
||||
<h3>General</h3>
|
||||
<h3>General</h3>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{computedTotalDisplay}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={activeType === 'total'}
|
||||
onclick={() => (activeType = 'total')}
|
||||
>
|
||||
total<span>({computedFps}fps)</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{#each performanceData as [key, value] (key)}
|
||||
<tr>
|
||||
<td>
|
||||
{round(getTotalPerformance(!showAverage))}<span>ms</span>
|
||||
</td>
|
||||
<td>{round(value)}<span>ms</span></td>
|
||||
<td
|
||||
class:active={$activeType === "total"}
|
||||
on:click={() => ($activeType = "total")}
|
||||
class:active={activeType === key}
|
||||
onclick={() => (activeType = key)}
|
||||
>
|
||||
total<span
|
||||
>({Math.floor(
|
||||
1000 / getTotalPerformance(showAverage),
|
||||
)}fps)</span
|
||||
>
|
||||
{key}
|
||||
</td>
|
||||
</tr>
|
||||
{#each getPerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<tr>
|
||||
<td>{data.length}</td>
|
||||
<td>Samples</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody>
|
||||
<tr><td><h3>Nodes</h3></td></tr>
|
||||
</tbody>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span>%</span></td>
|
||||
<td
|
||||
class:active={activeType === 'cache-hit'}
|
||||
onclick={() => (activeType = 'cache-hit')}
|
||||
>
|
||||
cache hits
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{#each nodePerformanceData as [key, value] (key)}
|
||||
<tr>
|
||||
<td>{data.length}</td>
|
||||
<td>Samples</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Nodes</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> {getCacheRatio(!showAverage)}<span>%</span> </td>
|
||||
<td>{round(value)}<span>ms</span></td>
|
||||
<td
|
||||
class:active={$activeType === "cache-hit"}
|
||||
on:click={() => ($activeType = "cache-hit")}>cache hits</td
|
||||
class:active={activeType === key}
|
||||
onclick={() => (activeType = key)}
|
||||
>
|
||||
</tr>
|
||||
{#each getNodePerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key.split("/").slice(-1).join("/")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Viewer</h3>
|
||||
{key.split('/').slice(-1).join('/')}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
<tbody>
|
||||
<tr><td><h3>Viewer</h3></td></tr>
|
||||
</tbody>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast('total-vertices'))}</td>
|
||||
<td>Vertices</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast('total-faces'))}</td>
|
||||
<td>Faces</td>
|
||||
</tr>
|
||||
|
||||
{#each viewerPerformanceData as [key, value] (key)}
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast("total-vertices"))}</td>
|
||||
<td>Vertices</td>
|
||||
<td>{round(value)}<span>ms</span></td>
|
||||
<td
|
||||
class:active={activeType === key}
|
||||
onclick={() => (activeType = key)}
|
||||
>
|
||||
{key.split('/').slice(-1).join('/')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast("total-faces"))}</td>
|
||||
<td>Faces</td>
|
||||
</tr>
|
||||
{#each getViewerPerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key.split("/").slice(-1).join("/")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<p>No runs available</p>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/key}
|
||||
{:else}
|
||||
<p>No runs available</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let points: number[];
|
||||
const { points }: { points: number[] } = $props();
|
||||
|
||||
function constructPath() {
|
||||
const path = $derived.by(() => {
|
||||
const max = Math.max(...points);
|
||||
const min = Math.min(...points);
|
||||
return points
|
||||
@@ -10,14 +10,12 @@
|
||||
const y = 100 - ((point - min) / (max - min)) * 100;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
.join(' ');
|
||||
});
|
||||
</script>
|
||||
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 100 100">
|
||||
{#key points}
|
||||
<polyline vector-effect="non-scaling-stroke" points={constructPath()} />
|
||||
{/key}
|
||||
<polyline vector-effect="non-scaling-stroke" points={path} />
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
@@ -27,7 +25,7 @@
|
||||
}
|
||||
polyline {
|
||||
fill: none;
|
||||
stroke: var(--layer-3);
|
||||
stroke: var(--color-layer-3);
|
||||
opacity: 1;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { humanizeDuration, humanizeNumber } from "$lib/helpers";
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import SmallGraph from "./SmallGraph.svelte";
|
||||
import type { PerformanceData, PerformanceStore } from "@nodarium/utils";
|
||||
import { humanizeDuration, humanizeNumber } from '$lib/helpers';
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
import type { PerformanceData, PerformanceStore } from '@nodarium/utils';
|
||||
import SmallGraph from './SmallGraph.svelte';
|
||||
|
||||
export let store: PerformanceStore;
|
||||
const { store, fps }: { store: PerformanceStore; fps: number[] } = $props();
|
||||
|
||||
const open = localStore("node.performance.small.open", {
|
||||
const open = localState('node.performance.small.open', {
|
||||
runtime: false,
|
||||
fps: false,
|
||||
fps: false
|
||||
});
|
||||
|
||||
$: vertices = $store?.at(-1)?.["total-vertices"]?.[0] || 0;
|
||||
$: faces = $store?.at(-1)?.["total-faces"]?.[0] || 0;
|
||||
$: runtime = $store?.at(-1)?.["runtime"]?.[0] || 0;
|
||||
const vertices = $derived($store?.at(-1)?.['total-vertices']?.[0] || 0);
|
||||
const faces = $derived($store?.at(-1)?.['total-faces']?.[0] || 0);
|
||||
const runtime = $derived($store?.at(-1)?.['runtime']?.[0] || 0);
|
||||
|
||||
function getPoints(data: PerformanceData, key: string) {
|
||||
return data?.map((run) => run[key]?.[0] || 0) || [];
|
||||
}
|
||||
|
||||
export let fps: number[] = [];
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr
|
||||
style="cursor:pointer;"
|
||||
on:click={() => ($open.runtime = !$open.runtime)}
|
||||
style="cursor: pointer"
|
||||
onclick={() => (open.value.runtime = !open.value.runtime)}
|
||||
>
|
||||
<td>{$open.runtime ? "-" : "+"} runtime </td>
|
||||
<td>{open.value.runtime ? '-' : '+'} runtime</td>
|
||||
<td>{humanizeDuration(runtime || 1000)}</td>
|
||||
</tr>
|
||||
{#if $open.runtime}
|
||||
{#if open.value.runtime}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<SmallGraph points={getPoints($store, "runtime")} />
|
||||
<SmallGraph points={getPoints($store, 'runtime')} />
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<tr style="cursor:pointer;" on:click={() => ($open.fps = !$open.fps)}>
|
||||
<td>{$open.fps ? "-" : "+"} fps </td>
|
||||
<tr
|
||||
style="cursor: pointer"
|
||||
onclick={() => (open.value.fps = !open.value.fps)}
|
||||
>
|
||||
<td>{open.value.fps ? '-' : '+'} fps</td>
|
||||
<td>
|
||||
{Math.floor(fps[fps.length - 1])}fps
|
||||
</td>
|
||||
</tr>
|
||||
{#if $open.fps}
|
||||
{#if open.value.fps}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<SmallGraph points={fps} />
|
||||
@@ -55,12 +56,12 @@
|
||||
{/if}
|
||||
|
||||
<tr>
|
||||
<td>vertices </td>
|
||||
<td>vertices</td>
|
||||
<td>{humanizeNumber(vertices || 0)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>faces </td>
|
||||
<td>faces</td>
|
||||
<td>{humanizeNumber(faces || 0)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -73,14 +74,14 @@
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
background: var(--layer-0);
|
||||
border: solid thin var(--outline);
|
||||
background: var(--color-layer-0);
|
||||
border: solid thin var(--color-outline);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
td {
|
||||
padding: 4px;
|
||||
padding-inline: 8px;
|
||||
font-size: 0.8em;
|
||||
border: solid thin var(--outline);
|
||||
border: solid thin var(--color-outline);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as PerformanceViewer } from "./PerformanceViewer.svelte";
|
||||
export { default as PerformanceViewer } from './PerformanceViewer.svelte';
|
||||
|
||||
107
app/src/lib/project-manager/ProjectManager.svelte
Normal file
107
app/src/lib/project-manager/ProjectManager.svelte
Normal file
@@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
import { defaultPlant, lottaFaces, plant } from '$lib/graph-templates';
|
||||
import type { Graph } from '$lib/types';
|
||||
import type { ProjectManager } from './project-manager.svelte';
|
||||
|
||||
const { projectManager } = $props<{ projectManager: ProjectManager }>();
|
||||
|
||||
let showNewProject = $state(false);
|
||||
let newProjectName = $state('');
|
||||
let selectedTemplate = $state('defaultPlant');
|
||||
|
||||
const templates = [
|
||||
{
|
||||
name: 'Default Plant',
|
||||
value: 'defaultPlant',
|
||||
graph: defaultPlant as unknown as Graph
|
||||
},
|
||||
{ name: 'Plant', value: 'plant', graph: plant as unknown as Graph },
|
||||
{
|
||||
name: 'Lotta Faces',
|
||||
value: 'lottaFaces',
|
||||
graph: lottaFaces as unknown as Graph
|
||||
}
|
||||
];
|
||||
|
||||
function handleCreate() {
|
||||
const template = templates.find((t) => t.value === selectedTemplate) || templates[0];
|
||||
projectManager.handleCreateProject(template.graph, newProjectName);
|
||||
newProjectName = '';
|
||||
showNewProject = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="flex justify-between px-4 h-[70px] border-b-1 border-outline items-center">
|
||||
<h3>Project</h3>
|
||||
<button
|
||||
class="px-3 py-1 bg-layer-0 rounded"
|
||||
onclick={() => (showNewProject = !showNewProject)}
|
||||
>
|
||||
New
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if showNewProject}
|
||||
<div class="flex flex-col px-4 py-3 border-b-1 border-outline gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newProjectName}
|
||||
placeholder="Project name"
|
||||
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
|
||||
onkeydown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
/>
|
||||
<select
|
||||
bind:value={selectedTemplate}
|
||||
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
|
||||
>
|
||||
{#each templates as template (template.name)}
|
||||
<option value={template.value}>{template.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
class="cursor-pointer self-end px-3 py-1 bg-blue-600 rounded"
|
||||
onclick={() => handleCreate()}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="p-4 text-white min-h-screen">
|
||||
{#if projectManager.loading}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
|
||||
<ul class="space-y-2">
|
||||
{#each projectManager.projects as project (project.id)}
|
||||
<li>
|
||||
<div
|
||||
class="
|
||||
w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
|
||||
.activeProjectId.value === project.id
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-800 hover:bg-gray-700'}
|
||||
"
|
||||
onclick={() => projectManager.handleSelectProject(project.id!)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) =>
|
||||
e.key === 'Enter'
|
||||
&& projectManager.handleSelectProject(project.id!)}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>{project.meta?.title || 'Untitled'}</span>
|
||||
<button
|
||||
class="text-red-400 hover:text-red-300"
|
||||
onclick={() => {
|
||||
projectManager.handleDeleteProject(project.id!);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
53
app/src/lib/project-manager/project-database.svelte.ts
Normal file
53
app/src/lib/project-manager/project-database.svelte.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import { type IDBPDatabase, openDB } from 'idb';
|
||||
|
||||
export interface GraphDatabase {
|
||||
projects: Graph;
|
||||
}
|
||||
|
||||
const DB_NAME = 'nodarium-graphs';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'graphs';
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<GraphDatabase>> | null = null;
|
||||
|
||||
export function getDB() {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<GraphDatabase>(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
export async function getGraph(id: number): Promise<Graph | undefined> {
|
||||
const db = await getDB();
|
||||
return db.get(STORE_NAME, id);
|
||||
}
|
||||
|
||||
export async function saveGraph(graph: Graph): Promise<Graph> {
|
||||
const db = await getDB();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
|
||||
await db.put(STORE_NAME, graph);
|
||||
return graph;
|
||||
}
|
||||
|
||||
export async function deleteGraph(id: number): Promise<void> {
|
||||
const db = await getDB();
|
||||
await db.delete(STORE_NAME, id);
|
||||
}
|
||||
|
||||
export async function getGraphs(): Promise<Graph[]> {
|
||||
const db = await getDB();
|
||||
return db.getAll(STORE_NAME);
|
||||
}
|
||||
|
||||
export async function clear(): Promise<void> {
|
||||
const db = await getDB();
|
||||
return db.clear(STORE_NAME);
|
||||
}
|
||||
85
app/src/lib/project-manager/project-manager.svelte.ts
Normal file
85
app/src/lib/project-manager/project-manager.svelte.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as templates from '$lib/graph-templates';
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import * as db from './project-database.svelte';
|
||||
|
||||
export class ProjectManager {
|
||||
public graph = $state<Graph>();
|
||||
private projects = $state<Graph[]>([]);
|
||||
private activeProjectId = localState<number | undefined>(
|
||||
'node.activeProjectId',
|
||||
undefined
|
||||
);
|
||||
public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value);
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
async saveGraph(g: Graph) {
|
||||
db.saveGraph(g);
|
||||
}
|
||||
|
||||
private async init() {
|
||||
await db.getDB();
|
||||
this.projects = await db.getGraphs();
|
||||
|
||||
if (this.activeProjectId.value !== undefined) {
|
||||
const loadedGraph = await db.getGraph(this.activeProjectId.value);
|
||||
if (loadedGraph) {
|
||||
this.graph = loadedGraph;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.graph) {
|
||||
if (this.projects?.length && this.projects[0]?.id !== undefined) {
|
||||
this.graph = this.projects[0];
|
||||
this.activeProjectId.value = this.graph.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.graph) {
|
||||
this.handleCreateProject();
|
||||
}
|
||||
}
|
||||
|
||||
public handleCreateProject(
|
||||
g: Graph = templates.defaultPlant as unknown as Graph,
|
||||
title: string = 'New Project'
|
||||
) {
|
||||
let id = g?.id || 0;
|
||||
while (this.projects.find((p) => p.id === id)) {
|
||||
id++;
|
||||
}
|
||||
|
||||
g.id = id;
|
||||
if (!g.meta) g.meta = {};
|
||||
if (!g.meta.title) g.meta.title = title;
|
||||
|
||||
db.saveGraph(g);
|
||||
this.projects = [...this.projects, g];
|
||||
this.handleSelectProject(id);
|
||||
}
|
||||
|
||||
public async handleDeleteProject(projectId: number) {
|
||||
await db.deleteGraph(projectId);
|
||||
if (this.projects.length === 1) {
|
||||
this.graph = undefined;
|
||||
this.projects = [];
|
||||
} else {
|
||||
this.projects = this.projects.filter((p) => p.id !== projectId);
|
||||
const id = this.projects[0].id;
|
||||
if (id !== undefined) {
|
||||
this.handleSelectProject(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async handleSelectProject(id: number) {
|
||||
if (this.activeProjectId.value !== id) {
|
||||
const project = await db.getGraph(id);
|
||||
this.graph = project;
|
||||
this.activeProjectId.value = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import { T, useTask } from "@threlte/core";
|
||||
import { OrbitControls } from "@threlte/extras";
|
||||
import { onMount } from "svelte";
|
||||
import { Vector3 } from "three";
|
||||
import type { PerspectiveCamera, Vector3Tuple } from "three";
|
||||
import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import localStore from '$lib/helpers/localStore';
|
||||
import { T, useTask } from '@threlte/core';
|
||||
import { OrbitControls } from '@threlte/extras';
|
||||
import { onMount } from 'svelte';
|
||||
import { Vector3 } from 'three';
|
||||
import type { PerspectiveCamera, Vector3Tuple } from 'three';
|
||||
import type { OrbitControls as OrbitControlsType } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
let camera = $state<PerspectiveCamera>();
|
||||
let controls = $state<OrbitControlsType>();
|
||||
@@ -20,9 +20,9 @@
|
||||
const cameraTransform = localStore<{
|
||||
camera: Vector3Tuple;
|
||||
target: Vector3Tuple;
|
||||
}>("nodes.camera.transform", {
|
||||
}>('nodes.camera.transform', {
|
||||
camera: [10, 10, 10],
|
||||
target: [0, 0, 0],
|
||||
target: [0, 0, 0]
|
||||
});
|
||||
|
||||
function saveCameraState() {
|
||||
@@ -33,7 +33,7 @@
|
||||
if (tPos.some((v) => isNaN(v)) || cPos.some((v) => isNaN(v))) return;
|
||||
$cameraTransform = {
|
||||
camera: cPos,
|
||||
target: tPos,
|
||||
target: tPos
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@
|
||||
|
||||
$effect(() => {
|
||||
if (
|
||||
center &&
|
||||
controls &&
|
||||
centerCamera &&
|
||||
(center.x !== controls.target.x ||
|
||||
center.y !== controls.target.y ||
|
||||
center.z !== controls.target.z) &&
|
||||
!isRunning
|
||||
center
|
||||
&& controls
|
||||
&& centerCamera
|
||||
&& (center.x !== controls.target.x
|
||||
|| center.y !== controls.target.y
|
||||
|| center.z !== controls.target.z)
|
||||
&& !isRunning
|
||||
) {
|
||||
isRunning = true;
|
||||
task.start();
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { T, useTask, useThrelte } from "@threlte/core";
|
||||
import { colors } from '$lib/graph-interface/graph/colors.svelte';
|
||||
import { T, useTask, useThrelte } from '@threlte/core';
|
||||
import { Grid, MeshLineGeometry, MeshLineMaterial, Text } from '@threlte/extras';
|
||||
import {
|
||||
Grid,
|
||||
MeshLineGeometry,
|
||||
MeshLineMaterial,
|
||||
Text,
|
||||
} from "@threlte/extras";
|
||||
import {
|
||||
type Group,
|
||||
type BufferGeometry,
|
||||
Vector3,
|
||||
type Vector3Tuple,
|
||||
Box3,
|
||||
type BufferGeometry,
|
||||
type Group,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
} from "three";
|
||||
import { appSettings } from "../settings/app-settings.svelte";
|
||||
import Camera from "./Camera.svelte";
|
||||
import { colors } from "$lib/graph-interface/graph/colors.svelte";
|
||||
Vector3,
|
||||
type Vector3Tuple
|
||||
} from 'three';
|
||||
import { appSettings } from '../settings/app-settings.svelte';
|
||||
import Camera from './Camera.svelte';
|
||||
|
||||
const { renderStage, invalidate: _invalidate } = useThrelte();
|
||||
|
||||
@@ -32,24 +27,29 @@
|
||||
lines,
|
||||
centerCamera,
|
||||
fps = $bindable(),
|
||||
scene = $bindable(),
|
||||
scene = $bindable()
|
||||
}: Props = $props();
|
||||
|
||||
let geometries = $state.raw<BufferGeometry[]>([]);
|
||||
let center = $state(new Vector3(0, 4, 0));
|
||||
|
||||
useTask(
|
||||
(delta) => {
|
||||
fps.push(1 / delta);
|
||||
fps = fps.slice(-100);
|
||||
},
|
||||
{ stage: renderStage, autoInvalidate: false },
|
||||
{ stage: renderStage, autoInvalidate: false }
|
||||
);
|
||||
|
||||
export const invalidate = function () {
|
||||
export const invalidate = function() {
|
||||
if (scene) {
|
||||
geometries = scene.children
|
||||
.filter((child) => "geometry" in child && child.isObject3D)
|
||||
.map((child) => {
|
||||
return (child as Mesh).geometry;
|
||||
});
|
||||
const geos: BufferGeometry[] = [];
|
||||
scene.traverse(function(child) {
|
||||
if (isMesh(child)) {
|
||||
geos.push(child.geometry);
|
||||
}
|
||||
});
|
||||
geometries = geos;
|
||||
}
|
||||
|
||||
if (geometries && scene && centerCamera) {
|
||||
@@ -62,21 +62,30 @@
|
||||
_invalidate();
|
||||
};
|
||||
|
||||
let geometries = $state<BufferGeometry[]>();
|
||||
let center = $state(new Vector3(0, 4, 0));
|
||||
|
||||
function isMesh(child: Mesh | any): child is Mesh {
|
||||
return child.isObject3D && "material" in child;
|
||||
function isMesh(child: unknown): child is Mesh {
|
||||
return (
|
||||
child !== null
|
||||
&& typeof child === 'object'
|
||||
&& 'isObject3D' in child
|
||||
&& child.isObject3D === true
|
||||
&& 'material' in child
|
||||
);
|
||||
}
|
||||
|
||||
function isMatCapMaterial(material: any): material is MeshBasicMaterial {
|
||||
return material.isMaterial && "matcap" in material;
|
||||
function isMatCapMaterial(material: unknown): material is MeshBasicMaterial {
|
||||
return (
|
||||
material !== null
|
||||
&& typeof material === 'object'
|
||||
&& 'isMaterial' in material
|
||||
&& material.isMaterial === true
|
||||
&& 'matcap' in material
|
||||
);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const wireframe = appSettings.value.debug.wireframe;
|
||||
scene.traverse(function (child) {
|
||||
if (isMesh(child) && isMatCapMaterial(child.material)) {
|
||||
scene.traverse(function(child) {
|
||||
if (isMesh(child) && isMatCapMaterial(child.material) && child.visible) {
|
||||
child.material.wireframe = wireframe;
|
||||
}
|
||||
});
|
||||
@@ -87,7 +96,7 @@
|
||||
return [
|
||||
geo.attributes.position.array[i],
|
||||
geo.attributes.position.array[i + 1],
|
||||
geo.attributes.position.array[i + 2],
|
||||
geo.attributes.position.array[i + 2]
|
||||
] as Vector3Tuple;
|
||||
}
|
||||
</script>
|
||||
@@ -96,12 +105,12 @@
|
||||
|
||||
{#if appSettings.value.showGrid}
|
||||
<Grid
|
||||
cellColor={colors["outline"]}
|
||||
cellColor={colors['outline']}
|
||||
cellThickness={0.7}
|
||||
infiniteGrid
|
||||
sectionThickness={0.7}
|
||||
sectionDistance={2}
|
||||
sectionColor={colors["outline"]}
|
||||
sectionColor={colors['outline']}
|
||||
fadeDistance={50}
|
||||
fadeStrength={10}
|
||||
fadeOrigin={new Vector3(0, 0, 0)}
|
||||
@@ -110,9 +119,9 @@
|
||||
|
||||
<T.Group>
|
||||
{#if geometries}
|
||||
{#each geometries as geo}
|
||||
{#each geometries as geo (geo.id)}
|
||||
{#if appSettings.value.debug.showIndices}
|
||||
{#each geo.attributes.position.array as _, i}
|
||||
{#each geo.attributes.position.array, i (i)}
|
||||
{#if i % 3 === 0}
|
||||
<Text fontSize={0.25} position={getPosition(geo, i)} />
|
||||
{/if}
|
||||
@@ -132,7 +141,7 @@
|
||||
</T.Group>
|
||||
|
||||
{#if appSettings.value.debug.showStemLines && lines}
|
||||
{#each lines as line}
|
||||
{#each lines as line (line[0].x + '-' + line[0].y + '-' + '' + line[0].z)}
|
||||
<T.Mesh>
|
||||
<MeshLineGeometry points={line} />
|
||||
<MeshLineMaterial width={0.1} color="red" depthTest={false} />
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Canvas } from "@threlte/core";
|
||||
import Scene from "./Scene.svelte";
|
||||
import { Vector3 } from "three";
|
||||
import { decodeFloat, splitNestedArray } from "@nodarium/utils";
|
||||
import type { PerformanceStore } from "@nodarium/utils";
|
||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
|
||||
import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
|
||||
import {
|
||||
createGeometryPool,
|
||||
createInstancedGeometryPool,
|
||||
} from "./geometryPool";
|
||||
import SmallPerformanceViewer from '$lib/performance/SmallPerformanceViewer.svelte';
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import { decodeFloat, splitNestedArray } from '@nodarium/utils';
|
||||
import type { PerformanceStore } from '@nodarium/utils';
|
||||
import { Canvas } from '@threlte/core';
|
||||
import { Vector3 } from 'three';
|
||||
import { type Group, MeshMatcapMaterial, TextureLoader } from 'three';
|
||||
import { createGeometryPool, createInstancedGeometryPool } from './geometryPool';
|
||||
import Scene from './Scene.svelte';
|
||||
|
||||
const loader = new TextureLoader();
|
||||
const matcap = loader.load("/matcap_green.jpg");
|
||||
matcap.colorSpace = "srgb";
|
||||
const matcap = loader.load('/matcap_green.jpg');
|
||||
matcap.colorSpace = 'srgb';
|
||||
const material = new MeshMatcapMaterial({
|
||||
color: 0xffffff,
|
||||
matcap,
|
||||
matcap
|
||||
});
|
||||
|
||||
let sceneComponent = $state<ReturnType<typeof Scene>>();
|
||||
@@ -34,7 +31,7 @@
|
||||
|
||||
return {
|
||||
totalFaces: meshes.totalFaces + faces.totalFaces,
|
||||
totalVertices: meshes.totalVertices + faces.totalVertices,
|
||||
totalVertices: meshes.totalVertices + faces.totalVertices
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,12 +61,12 @@
|
||||
}
|
||||
|
||||
export const update = function update(result: Int32Array) {
|
||||
perf.addPoint("split-result");
|
||||
perf.addPoint('split-result');
|
||||
const inputs = splitNestedArray(result);
|
||||
perf.endPoint();
|
||||
|
||||
if (appSettings.value.debug.showStemLines) {
|
||||
perf.addPoint("create-lines");
|
||||
perf.addPoint('create-lines');
|
||||
lines = inputs
|
||||
.map((input) => {
|
||||
if (input[0] === 0) {
|
||||
@@ -80,13 +77,13 @@
|
||||
perf.endPoint();
|
||||
}
|
||||
|
||||
perf.addPoint("update-geometries");
|
||||
perf.addPoint('update-geometries');
|
||||
|
||||
const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
|
||||
perf.endPoint();
|
||||
|
||||
perf.addPoint("total-vertices", totalVertices);
|
||||
perf.addPoint("total-faces", totalFaces);
|
||||
perf.addPoint('total-vertices', totalVertices);
|
||||
perf.addPoint('total-faces', totalFaces);
|
||||
sceneComponent?.invalidate();
|
||||
};
|
||||
</script>
|
||||
@@ -95,12 +92,14 @@
|
||||
<SmallPerformanceViewer {fps} store={perf} />
|
||||
{/if}
|
||||
|
||||
<Canvas>
|
||||
<Scene
|
||||
bind:this={sceneComponent}
|
||||
{lines}
|
||||
{centerCamera}
|
||||
bind:scene
|
||||
bind:fps
|
||||
/>
|
||||
</Canvas>
|
||||
<div style="height: 100%">
|
||||
<Canvas>
|
||||
<Scene
|
||||
bind:this={sceneComponent}
|
||||
{lines}
|
||||
{centerCamera}
|
||||
bind:scene
|
||||
bind:fps
|
||||
/>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fastHashArrayBuffer } from "@nodarium/utils";
|
||||
import { fastHashArrayBuffer } from '@nodarium/utils';
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
InstancedMesh,
|
||||
Material,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
} from "three";
|
||||
Mesh
|
||||
} from 'three';
|
||||
|
||||
function fastArrayHash(arr: Int32Array) {
|
||||
const sampleDistance = Math.max(Math.floor(arr.length / 100), 1);
|
||||
const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1);
|
||||
const sampleCount = Math.floor(arr.length / sampleDistance);
|
||||
|
||||
let hash = new Int32Array(sampleCount);
|
||||
const hash = new Int32Array(sampleCount);
|
||||
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
const index = i * sampleDistance;
|
||||
@@ -28,18 +28,21 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
||||
const scene = new Group();
|
||||
parentScene.add(scene);
|
||||
|
||||
let meshes: Mesh[] = [];
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
let totalVertices = 0;
|
||||
let totalFaces = 0;
|
||||
|
||||
function updateSingleGeometry(
|
||||
data: Int32Array,
|
||||
existingMesh: Mesh | null = null,
|
||||
existingMesh: Mesh | null = null
|
||||
) {
|
||||
let hash = fastArrayHash(data);
|
||||
const hash = fastArrayHash(data);
|
||||
|
||||
let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
|
||||
const geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
|
||||
if (existingMesh) {
|
||||
existingMesh.visible = true;
|
||||
}
|
||||
|
||||
// Extract data from the encoded array
|
||||
// const geometryType = encodedData[index++];
|
||||
@@ -62,8 +65,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
||||
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
||||
index = index + vertexCount * 3;
|
||||
|
||||
let posAttribute = geometry.getAttribute(
|
||||
"position",
|
||||
const posAttribute = geometry.getAttribute(
|
||||
'position'
|
||||
) as BufferAttribute | null;
|
||||
|
||||
if (posAttribute && posAttribute.count === vertexCount) {
|
||||
@@ -71,8 +74,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
||||
posAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new Float32BufferAttribute(vertices, 3),
|
||||
'position',
|
||||
new Float32BufferAttribute(vertices, 3)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,27 +83,27 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
||||
index = index + vertexCount * 3;
|
||||
|
||||
if (
|
||||
geometry.userData?.faceCount !== faceCount ||
|
||||
geometry.userData?.vertexCount !== vertexCount
|
||||
geometry.userData?.faceCount !== faceCount
|
||||
|| geometry.userData?.vertexCount !== vertexCount
|
||||
) {
|
||||
// Add data to geometry
|
||||
geometry.setIndex([...indices]);
|
||||
}
|
||||
|
||||
const normalsAttribute = geometry.getAttribute(
|
||||
"normal",
|
||||
'normal'
|
||||
) as BufferAttribute | null;
|
||||
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
||||
normalsAttribute.set(normals, 0);
|
||||
normalsAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
|
||||
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
|
||||
}
|
||||
|
||||
geometry.userData = {
|
||||
vertexCount,
|
||||
faceCount,
|
||||
hash,
|
||||
hash
|
||||
};
|
||||
|
||||
if (!existingMesh) {
|
||||
@@ -116,22 +119,21 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
||||
totalFaces = 0;
|
||||
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
|
||||
const existingMesh = meshes[i];
|
||||
let input = newData[i];
|
||||
const input = newData[i];
|
||||
if (input) {
|
||||
updateSingleGeometry(input, existingMesh || null);
|
||||
} else if (existingMesh) {
|
||||
existingMesh.visible = false;
|
||||
scene.remove(existingMesh);
|
||||
}
|
||||
}
|
||||
return { totalVertices, totalFaces };
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createInstancedGeometryPool(
|
||||
parentScene: Group,
|
||||
material: Material,
|
||||
material: Material
|
||||
) {
|
||||
const scene = new Group();
|
||||
parentScene.add(scene);
|
||||
@@ -142,11 +144,11 @@ export function createInstancedGeometryPool(
|
||||
|
||||
function updateSingleInstance(
|
||||
data: Int32Array,
|
||||
existingInstance: InstancedMesh | null = null,
|
||||
existingInstance: InstancedMesh | null = null
|
||||
) {
|
||||
let hash = fastArrayHash(data);
|
||||
const hash = fastArrayHash(data);
|
||||
|
||||
let geometry = existingInstance
|
||||
const geometry = existingInstance
|
||||
? existingInstance.geometry
|
||||
: new BufferGeometry();
|
||||
|
||||
@@ -167,8 +169,8 @@ export function createInstancedGeometryPool(
|
||||
const indices = data.subarray(index, indicesEnd);
|
||||
index = indicesEnd;
|
||||
if (
|
||||
geometry.userData?.faceCount !== faceCount ||
|
||||
geometry.userData?.vertexCount !== vertexCount
|
||||
geometry.userData?.faceCount !== faceCount
|
||||
|| geometry.userData?.vertexCount !== vertexCount
|
||||
) {
|
||||
// Add data to geometry
|
||||
geometry.setIndex([...indices]);
|
||||
@@ -177,34 +179,34 @@ export function createInstancedGeometryPool(
|
||||
// Vertices
|
||||
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
||||
index = index + vertexCount * 3;
|
||||
let posAttribute = geometry.getAttribute(
|
||||
"position",
|
||||
const posAttribute = geometry.getAttribute(
|
||||
'position'
|
||||
) as BufferAttribute | null;
|
||||
if (posAttribute && posAttribute.count === vertexCount) {
|
||||
posAttribute.set(vertices, 0);
|
||||
posAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new Float32BufferAttribute(vertices, 3),
|
||||
'position',
|
||||
new Float32BufferAttribute(vertices, 3)
|
||||
);
|
||||
}
|
||||
|
||||
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
||||
index = index + vertexCount * 3;
|
||||
const normalsAttribute = geometry.getAttribute(
|
||||
"normal",
|
||||
'normal'
|
||||
) as BufferAttribute | null;
|
||||
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
||||
normalsAttribute.set(normals, 0);
|
||||
normalsAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
|
||||
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
|
||||
}
|
||||
|
||||
if (
|
||||
existingInstance &&
|
||||
instanceCount > existingInstance.geometry.userData.count
|
||||
existingInstance
|
||||
&& instanceCount > existingInstance.geometry.userData.count
|
||||
) {
|
||||
scene.remove(existingInstance);
|
||||
instances.splice(instances.indexOf(existingInstance), 1);
|
||||
@@ -224,12 +226,12 @@ export function createInstancedGeometryPool(
|
||||
const matrices = new Float32Array(
|
||||
data.buffer,
|
||||
index * 4,
|
||||
instanceCount * 16,
|
||||
instanceCount * 16
|
||||
);
|
||||
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
const matrix = new Matrix4().fromArray(
|
||||
matrices.subarray(i * 16, i * 16 + 16),
|
||||
matrices.subarray(i * 16, i * 16 + 16)
|
||||
);
|
||||
existingInstance.setMatrixAt(i, matrix);
|
||||
}
|
||||
@@ -239,9 +241,9 @@ export function createInstancedGeometryPool(
|
||||
faceCount,
|
||||
count: Math.max(
|
||||
instanceCount,
|
||||
existingInstance.geometry.userData.count || 0,
|
||||
existingInstance.geometry.userData.count || 0
|
||||
),
|
||||
hash,
|
||||
hash
|
||||
};
|
||||
|
||||
existingInstance.instanceMatrix.needsUpdate = true;
|
||||
@@ -253,15 +255,14 @@ export function createInstancedGeometryPool(
|
||||
totalFaces = 0;
|
||||
for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
|
||||
const existingMesh = instances[i];
|
||||
let input = newData[i];
|
||||
const input = newData[i];
|
||||
if (input) {
|
||||
updateSingleInstance(input, existingMesh || null);
|
||||
} else if (existingMesh) {
|
||||
existingMesh.visible = false;
|
||||
scene.remove(existingMesh);
|
||||
}
|
||||
}
|
||||
return { totalVertices, totalFaces };
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./runtime-executor"
|
||||
export * from "./runtime-executor-cache"
|
||||
export * from "./worker-runtime-executor"
|
||||
|
||||
export * from './runtime-executor';
|
||||
export * from './runtime-executor-cache';
|
||||
export * from './worker-runtime-executor';
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Graph, RuntimeExecutor } from "@nodarium/types";
|
||||
import type { Graph, RuntimeExecutor } from '@nodarium/types';
|
||||
|
||||
export class RemoteRuntimeExecutor implements RuntimeExecutor {
|
||||
constructor(private url: string) {}
|
||||
|
||||
constructor(private url: string) { }
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, any>): Promise<Int32Array> {
|
||||
|
||||
const res = await fetch(this.url, { method: "POST", body: JSON.stringify({ graph, settings }) });
|
||||
async execute(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
|
||||
const res = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ graph, settings })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to execute graph`);
|
||||
}
|
||||
|
||||
return new Int32Array(await res.arrayBuffer());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import { type SyncCache } from "@nodarium/types";
|
||||
import { type SyncCache } from '@nodarium/types';
|
||||
|
||||
export class MemoryRuntimeCache implements SyncCache {
|
||||
private map = new Map<string, unknown>();
|
||||
size: number;
|
||||
|
||||
private cache: [string, unknown][] = [];
|
||||
size = 50;
|
||||
constructor(size = 50) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.cache.find(([k]) => k === key)?.[1] as T;
|
||||
}
|
||||
set<T>(key: string, value: T): void {
|
||||
this.cache.push([key, value]);
|
||||
this.cache = this.cache.slice(-this.size);
|
||||
}
|
||||
clear(): void {
|
||||
this.cache = [];
|
||||
if (!this.map.has(key)) return undefined;
|
||||
const value = this.map.get(key) as T;
|
||||
this.map.delete(key);
|
||||
this.map.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
if (this.map.has(key)) {
|
||||
this.map.delete(key);
|
||||
}
|
||||
this.map.set(key, value);
|
||||
while (this.map.size > this.size) {
|
||||
const oldestKey = this.map.keys().next().value as string;
|
||||
this.map.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import type {
|
||||
Graph,
|
||||
Node,
|
||||
NodeDefinition,
|
||||
NodeInput,
|
||||
NodeRegistry,
|
||||
RuntimeExecutor,
|
||||
SyncCache,
|
||||
} from "@nodarium/types";
|
||||
SyncCache
|
||||
} from '@nodarium/types';
|
||||
import {
|
||||
concatEncodedArrays,
|
||||
createLogger,
|
||||
encodeFloat,
|
||||
fastHashArrayBuffer,
|
||||
type PerformanceStore,
|
||||
} from "@nodarium/utils";
|
||||
type PerformanceStore
|
||||
} from '@nodarium/utils';
|
||||
import type { RuntimeNode } from './types';
|
||||
|
||||
const log = createLogger("runtime-executor");
|
||||
const log = createLogger('runtime-executor');
|
||||
log.mute();
|
||||
|
||||
function getValue(input: NodeInput, value?: unknown) {
|
||||
if (value === undefined && "value" in input) {
|
||||
if (value === undefined && 'value' in input) {
|
||||
value = input.value;
|
||||
}
|
||||
|
||||
if (input.type === "float") {
|
||||
if (input.type === 'float') {
|
||||
return encodeFloat(value as number);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (input.type === "vec3") {
|
||||
if (input.type === 'vec3') {
|
||||
return [
|
||||
0,
|
||||
value.length + 1,
|
||||
...value.map((v) => encodeFloat(v)),
|
||||
1,
|
||||
1,
|
||||
1
|
||||
] as number[];
|
||||
}
|
||||
return [0, value.length + 1, ...value, 1, 1] as number[];
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
|
||||
constructor(
|
||||
private registry: NodeRegistry,
|
||||
private cache?: SyncCache<Int32Array>,
|
||||
public cache?: SyncCache<Int32Array>
|
||||
) {
|
||||
this.cache = undefined;
|
||||
}
|
||||
|
||||
private async getNodeDefinitions(graph: Graph) {
|
||||
if (this.registry.status !== "ready") {
|
||||
throw new Error("Node registry is not ready");
|
||||
if (this.registry.status !== 'ready') {
|
||||
throw new Error('Node registry is not ready');
|
||||
}
|
||||
|
||||
await this.registry.load(graph.nodes.map((node) => node.type));
|
||||
@@ -92,34 +92,35 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
// First, lets check if all nodes have a definition
|
||||
this.definitionMap = await this.getNodeDefinitions(graph);
|
||||
|
||||
const outputNode = graph.nodes.find((node) =>
|
||||
node.type.endsWith("/output"),
|
||||
) as Node;
|
||||
const graphNodes = graph.nodes.map(node => {
|
||||
const n = node as RuntimeNode;
|
||||
n.state = {
|
||||
depth: 0,
|
||||
children: [],
|
||||
parents: [],
|
||||
inputNodes: {}
|
||||
};
|
||||
return n;
|
||||
});
|
||||
|
||||
const outputNode = graphNodes.find((node) => node.type.endsWith('/output'));
|
||||
if (!outputNode) {
|
||||
throw new Error("No output node found");
|
||||
throw new Error('No output node found');
|
||||
}
|
||||
|
||||
outputNode.tmp = outputNode.tmp || {};
|
||||
outputNode.tmp.depth = 0;
|
||||
|
||||
const nodeMap = new Map<number, Node>(
|
||||
graph.nodes.map((node) => [node.id, node]),
|
||||
const nodeMap = new Map(
|
||||
graphNodes.map((node) => [node.id, node])
|
||||
);
|
||||
|
||||
// loop through all edges and assign the parent and child nodes to each node
|
||||
for (const edge of graph.edges) {
|
||||
const [parentId, _parentOutput, childId, childInput] = edge;
|
||||
const [parentId, /*_parentOutput*/, childId, childInput] = edge;
|
||||
const parent = nodeMap.get(parentId);
|
||||
const child = nodeMap.get(childId);
|
||||
if (parent && child) {
|
||||
parent.tmp = parent.tmp || {};
|
||||
parent.tmp.children = parent.tmp.children || [];
|
||||
parent.tmp.children.push(child);
|
||||
child.tmp = child.tmp || {};
|
||||
child.tmp.parents = child.tmp.parents || [];
|
||||
child.tmp.parents.push(parent);
|
||||
child.tmp.inputNodes = child.tmp.inputNodes || {};
|
||||
child.tmp.inputNodes[childInput] = parent;
|
||||
parent.state.children.push(child);
|
||||
child.state.parents.push(parent);
|
||||
child.state.inputNodes[childInput] = parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,20 +131,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
while (stack.length) {
|
||||
const node = stack.pop();
|
||||
if (!node) continue;
|
||||
node.tmp = node.tmp || {};
|
||||
if (node?.tmp?.depth === undefined) {
|
||||
node.tmp.depth = 0;
|
||||
}
|
||||
if (node?.tmp?.parents !== undefined) {
|
||||
for (const parent of node.tmp.parents) {
|
||||
parent.tmp = parent.tmp || {};
|
||||
if (parent.tmp?.depth === undefined) {
|
||||
parent.tmp.depth = node.tmp.depth + 1;
|
||||
stack.push(parent);
|
||||
} else {
|
||||
parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
|
||||
}
|
||||
}
|
||||
for (const parent of node.state.parents) {
|
||||
parent.state = parent.state || {};
|
||||
parent.state.depth = node.state.depth + 1;
|
||||
stack.push(parent);
|
||||
}
|
||||
nodes.push(node);
|
||||
}
|
||||
@@ -152,7 +143,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
}
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
this.perf?.addPoint("runtime");
|
||||
this.perf?.addPoint('runtime');
|
||||
|
||||
let a = performance.now();
|
||||
|
||||
@@ -160,7 +151,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
||||
let b = performance.now();
|
||||
|
||||
this.perf?.addPoint("collect-metadata", b - a);
|
||||
this.perf?.addPoint('collect-metadata', b - a);
|
||||
|
||||
/*
|
||||
* Here we sort the nodes into buckets, which we then execute one by one
|
||||
@@ -175,20 +166,20 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
|
||||
// we execute the nodes from the bottom up
|
||||
const sortedNodes = nodes.sort(
|
||||
(a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0),
|
||||
(a, b) => (b.state?.depth || 0) - (a.state?.depth || 0)
|
||||
);
|
||||
|
||||
// here we store the intermediate results of the nodes
|
||||
const results: Record<string, Int32Array> = {};
|
||||
|
||||
if (settings["randomSeed"]) {
|
||||
if (settings['randomSeed']) {
|
||||
this.seed = Math.floor(Math.random() * 100000000);
|
||||
}
|
||||
|
||||
for (const node of sortedNodes) {
|
||||
const node_type = this.definitionMap.get(node.type)!;
|
||||
|
||||
if (!node_type || !node.tmp || !node_type.execute) {
|
||||
if (!node_type || !node.state || !node_type.execute) {
|
||||
log.warn(`Node ${node.id} has no definition`);
|
||||
continue;
|
||||
}
|
||||
@@ -198,7 +189,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
// Collect the inputs for the node
|
||||
const inputs = Object.entries(node_type.inputs || {}).map(
|
||||
([key, input]) => {
|
||||
if (input.type === "seed") {
|
||||
if (input.type === 'seed') {
|
||||
return this.seed;
|
||||
}
|
||||
|
||||
@@ -208,11 +199,11 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
}
|
||||
|
||||
// check if the input is connected to another node
|
||||
const inputNode = node.tmp?.inputNodes?.[key];
|
||||
const inputNode = node.state.inputNodes[key];
|
||||
if (inputNode) {
|
||||
if (results[inputNode.id] === undefined) {
|
||||
throw new Error(
|
||||
`Node ${node.type} is missing input from node ${inputNode.type}`,
|
||||
`Node ${node.type} is missing input from node ${inputNode.type}`
|
||||
);
|
||||
}
|
||||
return results[inputNode.id];
|
||||
@@ -224,44 +215,45 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
}
|
||||
|
||||
return getValue(input);
|
||||
},
|
||||
}
|
||||
);
|
||||
b = performance.now();
|
||||
|
||||
this.perf?.addPoint("collected-inputs", b - a);
|
||||
this.perf?.addPoint('collected-inputs', b - a);
|
||||
|
||||
try {
|
||||
a = performance.now();
|
||||
const encoded_inputs = concatEncodedArrays(inputs);
|
||||
b = performance.now();
|
||||
this.perf?.addPoint("encoded-inputs", b - a);
|
||||
this.perf?.addPoint('encoded-inputs', b - a);
|
||||
|
||||
a = performance.now();
|
||||
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
|
||||
const inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
|
||||
b = performance.now();
|
||||
this.perf?.addPoint("hash-inputs", b - a);
|
||||
this.perf?.addPoint('hash-inputs', b - a);
|
||||
|
||||
let cachedValue = this.cache?.get(inputHash);
|
||||
const cachedValue = this.cache?.get(inputHash);
|
||||
if (cachedValue !== undefined) {
|
||||
log.log(`Using cached value for ${node_type.id || node.id}`);
|
||||
this.perf?.addPoint("cache-hit", 1);
|
||||
this.perf?.addPoint('cache-hit', 1);
|
||||
results[node.id] = cachedValue as Int32Array;
|
||||
continue;
|
||||
}
|
||||
this.perf?.addPoint("cache-hit", 0);
|
||||
this.perf?.addPoint('cache-hit', 0);
|
||||
|
||||
log.group(`executing ${node_type.id || node.id}`);
|
||||
log.group(`executing ${node_type.id}-${node.id}`);
|
||||
log.log(`Inputs:`, inputs);
|
||||
a = performance.now();
|
||||
results[node.id] = node_type.execute(encoded_inputs);
|
||||
log.log('Executed', node.type, node.id);
|
||||
b = performance.now();
|
||||
|
||||
if (this.cache) {
|
||||
if (this.cache && node.id !== outputNode.id) {
|
||||
this.cache.set(inputHash, results[node.id]);
|
||||
}
|
||||
|
||||
this.perf?.addPoint("node/" + node_type.id, b - a);
|
||||
log.log("Result:", results[node.id]);
|
||||
this.perf?.addPoint('node/' + node_type.id, b - a);
|
||||
log.log('Result:', results[node.id]);
|
||||
log.groupEnd();
|
||||
} catch (e) {
|
||||
log.groupEnd();
|
||||
@@ -276,7 +268,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
this.cache.size = sortedNodes.length * 2;
|
||||
}
|
||||
|
||||
this.perf?.endPoint("runtime");
|
||||
this.perf?.endPoint('runtime');
|
||||
|
||||
return res as unknown as Int32Array;
|
||||
}
|
||||
|
||||
10
app/src/lib/runtime/types.ts
Normal file
10
app/src/lib/runtime/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { SerializedNode } from '@nodarium/types';
|
||||
|
||||
type RuntimeState = {
|
||||
depth: number;
|
||||
parents: RuntimeNode[];
|
||||
children: RuntimeNode[];
|
||||
inputNodes: Record<string, RuntimeNode>;
|
||||
};
|
||||
|
||||
export type RuntimeNode = SerializedNode & { state: RuntimeState };
|
||||
@@ -1,23 +1,41 @@
|
||||
import { MemoryRuntimeExecutor } from "./runtime-executor";
|
||||
import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
|
||||
import type { Graph } from "@nodarium/types";
|
||||
import { createPerformanceStore } from "@nodarium/utils";
|
||||
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import { createPerformanceStore } from '@nodarium/utils';
|
||||
import { MemoryRuntimeExecutor } from './runtime-executor';
|
||||
import { MemoryRuntimeCache } from './runtime-executor-cache';
|
||||
|
||||
const indexDbCache = new IndexDBCache("node-registry");
|
||||
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
|
||||
const indexDbCache = new IndexDBCache('node-registry');
|
||||
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache);
|
||||
|
||||
const executor = new MemoryRuntimeExecutor(nodeRegistry);
|
||||
const cache = new MemoryRuntimeCache();
|
||||
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
|
||||
|
||||
const performanceStore = createPerformanceStore();
|
||||
executor.perf = performanceStore;
|
||||
|
||||
export async function setUseRegistryCache(useCache: boolean) {
|
||||
if (useCache) {
|
||||
nodeRegistry.cache = indexDbCache;
|
||||
} else {
|
||||
nodeRegistry.cache = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUseRuntimeCache(useCache: boolean) {
|
||||
if (useCache) {
|
||||
executor.cache = cache;
|
||||
} else {
|
||||
executor.cache = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeGraph(
|
||||
graph: Graph,
|
||||
settings: Record<string, unknown>,
|
||||
settings: Record<string, unknown>
|
||||
): Promise<Int32Array> {
|
||||
await nodeRegistry.load(graph.nodes.map((n) => n.type));
|
||||
performanceStore.startRun();
|
||||
let res = await executor.execute(graph, settings);
|
||||
const res = await executor.execute(graph, settings);
|
||||
performanceStore.stopRun();
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/// <reference types="vite-plugin-comlink/client" />
|
||||
import type { Graph, RuntimeExecutor } from "@nodarium/types";
|
||||
|
||||
import type { Graph, RuntimeExecutor } from '@nodarium/types';
|
||||
|
||||
export class WorkerRuntimeExecutor implements RuntimeExecutor {
|
||||
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url));
|
||||
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(
|
||||
new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)
|
||||
);
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
return this.worker.executeGraph(graph, settings);
|
||||
@@ -11,5 +12,10 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
|
||||
async getPerformanceData() {
|
||||
return this.worker.getPerformanceData();
|
||||
}
|
||||
set useRuntimeCache(useCache: boolean) {
|
||||
this.worker.setUseRuntimeCache(useCache);
|
||||
}
|
||||
set useRegistryCache(useCache: boolean) {
|
||||
this.worker.setUseRegistryCache(useCache);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import NestedSettings from "./NestedSettings.svelte";
|
||||
import { localState } from "$lib/helpers/localState.svelte";
|
||||
import type { NodeInput } from "@nodarium/types";
|
||||
import Input from "@nodarium/ui";
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
import type { NodeInput } from '@nodarium/types';
|
||||
import Input from '@nodarium/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import NestedSettings from './NestedSettings.svelte';
|
||||
|
||||
type Button = { type: "button"; callback: () => void; label?: string };
|
||||
type Button = { type: 'button'; label?: string };
|
||||
|
||||
type InputType = NodeInput | Button;
|
||||
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
interface SettingsGroup {
|
||||
title?: string;
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type SettingsType = Record<string, SettingsNode>;
|
||||
@@ -31,44 +32,49 @@
|
||||
};
|
||||
|
||||
// Local persistent state for <details> sections
|
||||
const openSections = localState<Record<string, boolean>>("open-details", {});
|
||||
const openSections = localState<Record<string, boolean>>('open-details', {});
|
||||
|
||||
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
|
||||
let { id, key = '', value = $bindable(), type, depth = 0 }: Props = $props();
|
||||
|
||||
function isNodeInput(v: SettingsNode | undefined): v is InputType {
|
||||
return !!v && typeof v === "object" && "type" in v;
|
||||
return !!v && typeof v === 'object' && 'type' in v;
|
||||
}
|
||||
|
||||
function getDefaultValue(): unknown {
|
||||
if (key === "" || key === "title") return;
|
||||
function getDefaultValue(): NodeInput['value'] | undefined {
|
||||
if (key === '' || key === 'title') return;
|
||||
|
||||
const node = type[key];
|
||||
const node = type[key] as SettingsNode;
|
||||
const inputValue = value[key];
|
||||
|
||||
if (!isNodeInput(node)) return;
|
||||
|
||||
const anyNode = node as any;
|
||||
|
||||
// select input: use index into options
|
||||
if (Array.isArray(anyNode.options)) {
|
||||
if (value?.[key] !== undefined) {
|
||||
return anyNode.options.indexOf(value[key]);
|
||||
if ('options' in node && Array.isArray(node.options)) {
|
||||
if (typeof inputValue === 'string') {
|
||||
return node.options.indexOf(inputValue);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (value?.[key] !== undefined) return value[key];
|
||||
// If the component is supplied with a default value use that
|
||||
if (inputValue !== undefined && typeof inputValue !== 'object') {
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
if ("value" in node && anyNode.value !== undefined) {
|
||||
return anyNode.value;
|
||||
if ('value' in node) {
|
||||
const nodeValue = node.value;
|
||||
if (nodeValue !== null && nodeValue !== undefined) {
|
||||
return nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "boolean":
|
||||
case 'boolean':
|
||||
return 0;
|
||||
case "float":
|
||||
case 'float':
|
||||
return 0.5;
|
||||
case "integer":
|
||||
case "select":
|
||||
case 'integer':
|
||||
case 'select':
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
@@ -77,52 +83,63 @@
|
||||
|
||||
let internalValue = $state(getDefaultValue());
|
||||
|
||||
let open = $state(openSections.value[id]);
|
||||
|
||||
// Persist <details> open/closed state for groups
|
||||
if (depth > 0 && !isNodeInput(type[key!])) {
|
||||
$effect(() => {
|
||||
if (open !== undefined) {
|
||||
openSections.value[id] = open;
|
||||
}
|
||||
});
|
||||
}
|
||||
let open = $state(false);
|
||||
|
||||
// Sync internalValue back into `value`
|
||||
$effect(() => {
|
||||
if (key === "" || internalValue === undefined) return;
|
||||
if (key === '' || internalValue === undefined) return;
|
||||
|
||||
const node = type[key];
|
||||
|
||||
if (
|
||||
isNodeInput(node) &&
|
||||
Array.isArray((node as any).options) &&
|
||||
typeof internalValue === "number"
|
||||
isNodeInput(node)
|
||||
&& 'options' in node
|
||||
&& Array.isArray(node.options)
|
||||
&& typeof internalValue === 'number'
|
||||
) {
|
||||
value[key] = (node as any)?.options?.[internalValue] as any;
|
||||
} else {
|
||||
value[key] = internalValue as any;
|
||||
value[key] = node?.options?.[internalValue];
|
||||
} else if (internalValue) {
|
||||
value[key] = internalValue;
|
||||
}
|
||||
});
|
||||
|
||||
function handleClick() {
|
||||
const callback = value[key] as unknown as () => void;
|
||||
callback();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
open = openSections.value[id];
|
||||
|
||||
// Persist <details> open/closed state for groups
|
||||
if (depth > 0 && !isNodeInput(type[key!])) {
|
||||
$effect(() => {
|
||||
if (open !== undefined) {
|
||||
openSections.value[id] = open;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if key && isNodeInput(type?.[key])}
|
||||
{@const inputType = type[key]}
|
||||
<!-- Leaf input -->
|
||||
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
|
||||
{#if type[key].type === "button"}
|
||||
<button onclick={() => "callback" in type[key] && type[key].callback()}>
|
||||
{type[key].label || key}
|
||||
<div class="input input-{inputType.type}" class:first-level={depth === 1}>
|
||||
{#if inputType.type === 'button'}
|
||||
<button onclick={handleClick}>
|
||||
{inputType.label || key}
|
||||
</button>
|
||||
{:else}
|
||||
{#if type[key].label !== ""}
|
||||
<label for={id}>{type[key].label || key}</label>
|
||||
{#if inputType.label !== ''}
|
||||
<label for={id}>{inputType.label || key}</label>
|
||||
{/if}
|
||||
<Input {id} input={type[key]} bind:value={internalValue} />
|
||||
<Input {id} input={inputType} bind:value={internalValue} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if depth === 0}
|
||||
<!-- Root: iterate over top-level keys -->
|
||||
{#each Object.keys(type ?? {}).filter((k) => k !== "title") as childKey}
|
||||
{#each Object.keys(type ?? {}).filter((k) => k !== 'title') as childKey (childKey)}
|
||||
<NestedSettings
|
||||
id={`${id}.${childKey}`}
|
||||
key={childKey}
|
||||
@@ -140,7 +157,7 @@
|
||||
<details bind:open>
|
||||
<summary><p>{(type[key] as SettingsGroup).title || key}</p></summary>
|
||||
<div class="content">
|
||||
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== "title") as childKey}
|
||||
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== 'title') as childKey (childKey)}
|
||||
<NestedSettings
|
||||
id={`${id}.${childKey}`}
|
||||
key={childKey}
|
||||
@@ -194,6 +211,11 @@
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
padding-bottom: 1px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.first-level.input-boolean {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -201,11 +223,10 @@
|
||||
}
|
||||
|
||||
hr {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,156 +1,163 @@
|
||||
import { localState } from "$lib/helpers/localState.svelte";
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
|
||||
const themes = [
|
||||
"dark",
|
||||
"light",
|
||||
"catppuccin",
|
||||
"solarized",
|
||||
"high-contrast",
|
||||
"nord",
|
||||
"dracula",
|
||||
'dark',
|
||||
'light',
|
||||
'catppuccin',
|
||||
'solarized',
|
||||
'high-contrast',
|
||||
'nord',
|
||||
'dracula'
|
||||
] as const;
|
||||
|
||||
export const AppSettingTypes = {
|
||||
theme: {
|
||||
type: "select",
|
||||
type: 'select',
|
||||
options: themes,
|
||||
label: "Theme",
|
||||
value: themes[0],
|
||||
label: 'Theme',
|
||||
value: themes[0]
|
||||
},
|
||||
showGrid: {
|
||||
type: "boolean",
|
||||
label: "Show Grid",
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
label: 'Show Grid',
|
||||
value: true
|
||||
},
|
||||
centerCamera: {
|
||||
type: "boolean",
|
||||
label: "Center Camera",
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
label: 'Center Camera',
|
||||
value: true
|
||||
},
|
||||
nodeInterface: {
|
||||
title: "Node Interface",
|
||||
title: 'Node Interface',
|
||||
showNodeGrid: {
|
||||
type: "boolean",
|
||||
label: "Show Grid",
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
label: 'Show Grid',
|
||||
value: true
|
||||
},
|
||||
snapToGrid: {
|
||||
type: "boolean",
|
||||
label: "Snap to Grid",
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
label: 'Snap to Grid',
|
||||
value: true
|
||||
},
|
||||
showHelp: {
|
||||
type: "boolean",
|
||||
label: "Show Help",
|
||||
value: false,
|
||||
},
|
||||
type: 'boolean',
|
||||
label: 'Show Help',
|
||||
value: false
|
||||
}
|
||||
},
|
||||
debug: {
|
||||
title: "Debug",
|
||||
title: 'Debug',
|
||||
wireframe: {
|
||||
type: "boolean",
|
||||
label: "Wireframe",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Wireframe',
|
||||
value: false
|
||||
},
|
||||
useWorker: {
|
||||
type: "boolean",
|
||||
label: "Execute runtime in worker",
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
label: 'Execute in WebWorker',
|
||||
value: true
|
||||
},
|
||||
showIndices: {
|
||||
type: "boolean",
|
||||
label: "Show Indices",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Show Indices',
|
||||
value: false
|
||||
},
|
||||
showPerformancePanel: {
|
||||
type: "boolean",
|
||||
label: "Show Performance Panel",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Show Performance Panel',
|
||||
value: false
|
||||
},
|
||||
showBenchmarkPanel: {
|
||||
type: "boolean",
|
||||
label: "Show Benchmark Panel",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Show Benchmark Panel',
|
||||
value: false
|
||||
},
|
||||
showVertices: {
|
||||
type: "boolean",
|
||||
label: "Show Vertices",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Show Vertices',
|
||||
value: false
|
||||
},
|
||||
showStemLines: {
|
||||
type: "boolean",
|
||||
label: "Show Stem Lines",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Show Stem Lines',
|
||||
value: false
|
||||
},
|
||||
showGraphJson: {
|
||||
type: "boolean",
|
||||
label: "Show Graph Source",
|
||||
value: false,
|
||||
type: 'boolean',
|
||||
label: 'Show Graph Source',
|
||||
value: false
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache',
|
||||
useRuntimeCache: {
|
||||
type: 'boolean',
|
||||
label: 'Node Results',
|
||||
value: true
|
||||
},
|
||||
useRegistryCache: {
|
||||
type: 'boolean',
|
||||
label: 'Node Source',
|
||||
value: true
|
||||
}
|
||||
},
|
||||
stressTest: {
|
||||
title: "Stress Test",
|
||||
title: 'Stress Test',
|
||||
amount: {
|
||||
type: "integer",
|
||||
type: 'integer',
|
||||
min: 2,
|
||||
max: 15,
|
||||
value: 4,
|
||||
value: 4
|
||||
},
|
||||
loadGrid: {
|
||||
type: "button",
|
||||
label: "Load Grid",
|
||||
type: 'button',
|
||||
label: 'Load Grid'
|
||||
},
|
||||
loadTree: {
|
||||
type: "button",
|
||||
label: "Load Tree",
|
||||
type: 'button',
|
||||
label: 'Load Tree'
|
||||
},
|
||||
lottaFaces: {
|
||||
type: "button",
|
||||
label: "Load 'lots of faces'",
|
||||
type: 'button',
|
||||
label: "Load 'lots of faces'"
|
||||
},
|
||||
lottaNodes: {
|
||||
type: "button",
|
||||
label: "Load 'lots of nodes'",
|
||||
type: 'button',
|
||||
label: "Load 'lots of nodes'"
|
||||
},
|
||||
lottaNodesAndFaces: {
|
||||
type: "button",
|
||||
label: "Load 'lots of nodes and faces'",
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'button',
|
||||
label: "Load 'lots of nodes and faces'"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
type SettingsToStore<T> =
|
||||
T extends { value: infer V }
|
||||
? V extends readonly string[]
|
||||
? V[number]
|
||||
: V
|
||||
: T extends any[]
|
||||
? {}
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T as T[K] extends object ? K : never]:
|
||||
SettingsToStore<T[K]>
|
||||
}
|
||||
type SettingsToStore<T> = T extends { type: 'button' } ? () => void
|
||||
: T extends { value: infer V } ? V extends readonly string[] ? V[number]
|
||||
: V
|
||||
: T extends object ? {
|
||||
-readonly [K in keyof T as T[K] extends object ? K : never]: SettingsToStore<T[K]>;
|
||||
}
|
||||
: never;
|
||||
|
||||
export function settingsToStore<T>(settings: T): SettingsToStore<T> {
|
||||
const result = {} as any;
|
||||
const result = {} as Record<string, unknown>;
|
||||
for (const key in settings) {
|
||||
const value = settings[key];
|
||||
if (value && typeof value === "object") {
|
||||
if ("value" in value) {
|
||||
if (value && typeof value === 'object') {
|
||||
if ('value' in value) {
|
||||
result[key] = value.value;
|
||||
} else {
|
||||
result[key] = settingsToStore(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result as SettingsToStore<T>;
|
||||
}
|
||||
|
||||
export let appSettings = localState(
|
||||
"app-settings",
|
||||
settingsToStore(AppSettingTypes),
|
||||
export const appSettings = localState(
|
||||
'app-settings',
|
||||
settingsToStore(AppSettingTypes)
|
||||
);
|
||||
|
||||
$effect.root(() => {
|
||||
@@ -160,7 +167,7 @@ $effect.root(() => {
|
||||
const newClassName = `theme-${theme}`;
|
||||
if (classes) {
|
||||
for (const className of classes) {
|
||||
if (className.startsWith("theme-") && className !== newClassName) {
|
||||
if (className.startsWith('theme-') && className !== newClassName) {
|
||||
classes.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NodeInput } from "@nodarium/types";
|
||||
import type { NodeInput } from '@nodarium/types';
|
||||
|
||||
type Button = { type: "button"; label?: string };
|
||||
type Button = { type: 'button'; label?: string };
|
||||
|
||||
export type SettingsStore = {
|
||||
[key: string]: SettingsStore | string | number | boolean;
|
||||
@@ -23,5 +23,5 @@ export type SettingsValue = Record<
|
||||
>;
|
||||
|
||||
export function isNodeInput(v: SettingsNode | undefined): v is InputType {
|
||||
return !!v && "type" in v;
|
||||
return !!v && 'type' in v;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Readable } from "svelte/store";
|
||||
import { type Snippet } from 'svelte';
|
||||
import { type Panel, panelState } from './PanelState.svelte';
|
||||
|
||||
export let id: string;
|
||||
export let icon: string = "";
|
||||
export let title = "";
|
||||
export let classes = "";
|
||||
export let hidden: boolean | undefined = undefined;
|
||||
const {
|
||||
id,
|
||||
icon = '',
|
||||
title = '',
|
||||
classes = '',
|
||||
hidden,
|
||||
children
|
||||
} = $props<{
|
||||
id: string;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
classes?: string;
|
||||
hidden?: boolean;
|
||||
children?: Snippet;
|
||||
}>();
|
||||
|
||||
const setVisibility =
|
||||
getContext<(id: string, visible: boolean) => void>("setVisibility");
|
||||
let panel = $state<Panel>(null!);
|
||||
|
||||
$: if (typeof hidden === "boolean") {
|
||||
setVisibility(id, !hidden);
|
||||
}
|
||||
$effect(() => {
|
||||
panelState.unregisterPanel(id);
|
||||
panel = panelState.registerPanel(id, icon, classes, hidden);
|
||||
});
|
||||
|
||||
const registerPanel =
|
||||
getContext<
|
||||
(id: string, icon: string, classes: string) => Readable<boolean>
|
||||
>("registerPanel");
|
||||
|
||||
let visible = registerPanel(id, icon, classes);
|
||||
$effect(() => {
|
||||
panel.hidden = hidden;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $visible}
|
||||
{#if panelState.activePanel.value === id}
|
||||
<div class="wrapper" class:hidden>
|
||||
{#if title}
|
||||
<header>
|
||||
<h3>{title}</h3>
|
||||
<header class="bg-layer-2">
|
||||
<h3 class="font-bold">{title}</h3>
|
||||
</header>
|
||||
{/if}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
border-bottom: solid thin var(--outline);
|
||||
height: 69px;
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1em;
|
||||
}
|
||||
h3 {
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
44
app/src/lib/sidebar/PanelState.svelte.ts
Normal file
44
app/src/lib/sidebar/PanelState.svelte.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
|
||||
export type Panel = {
|
||||
icon: string;
|
||||
classes: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
class PanelState {
|
||||
panels = $state<Record<string, Panel>>({});
|
||||
activePanel = localState<string | boolean>('node.activePanel', '');
|
||||
|
||||
get keys() {
|
||||
return Object.keys(this.panels);
|
||||
}
|
||||
|
||||
public unregisterPanel(id: string) {
|
||||
delete this.panels[id];
|
||||
}
|
||||
|
||||
public registerPanel(id: string, icon: string, classes: string, hidden: boolean): Panel {
|
||||
const state = $state({
|
||||
icon: icon,
|
||||
classes: classes,
|
||||
hidden: hidden
|
||||
});
|
||||
this.panels[id] = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
public toggleOpen() {
|
||||
if (this.activePanel.value) {
|
||||
this.activePanel.value = false;
|
||||
} else {
|
||||
this.activePanel.value = this.keys[0];
|
||||
}
|
||||
}
|
||||
|
||||
public setActivePanel(panelId: string) {
|
||||
this.activePanel.value = panelId;
|
||||
}
|
||||
}
|
||||
|
||||
export const panelState = new PanelState();
|
||||
@@ -1,77 +1,30 @@
|
||||
<script lang="ts">
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import { setContext } from "svelte";
|
||||
import { derived } from "svelte/store";
|
||||
import { type Snippet } from 'svelte';
|
||||
import { panelState as state } from './PanelState.svelte';
|
||||
|
||||
let panels: Record<
|
||||
string,
|
||||
{
|
||||
icon: string;
|
||||
id: string;
|
||||
classes: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
> = {};
|
||||
|
||||
const activePanel = localStore<keyof typeof panels | false>(
|
||||
"nodes.settings.activePanel",
|
||||
false,
|
||||
);
|
||||
|
||||
$: keys = panels
|
||||
? (Object.keys(panels) as unknown as (keyof typeof panels)[]).filter(
|
||||
(key) => !!panels[key]?.id,
|
||||
)
|
||||
: [];
|
||||
|
||||
setContext("setVisibility", (id: string, visible: boolean) => {
|
||||
panels[id].visible = visible;
|
||||
panels = { ...panels };
|
||||
});
|
||||
|
||||
setContext("registerPanel", (id: string, icon: string, classes: string) => {
|
||||
panels[id] = { id, icon, classes };
|
||||
return derived(activePanel, ($activePanel) => {
|
||||
return $activePanel === id;
|
||||
});
|
||||
});
|
||||
|
||||
function setActivePanel(panel: keyof typeof panels | false) {
|
||||
if (panel === $activePanel) {
|
||||
$activePanel = false;
|
||||
} else if (panel) {
|
||||
$activePanel = panel;
|
||||
} else {
|
||||
$activePanel = false;
|
||||
}
|
||||
}
|
||||
const { children } = $props<{ children?: Snippet }>();
|
||||
</script>
|
||||
|
||||
<div class="wrapper" class:visible={$activePanel}>
|
||||
<div class="wrapper" class:visible={state.activePanel.value}>
|
||||
<div class="tabs">
|
||||
<button
|
||||
aria-label="Close"
|
||||
on:click={() => {
|
||||
setActivePanel($activePanel ? false : keys[0]);
|
||||
}}
|
||||
>
|
||||
<span class="absolute i-tabler-chevron-left w-6 h-6 block"></span>
|
||||
<button aria-label="Close" onclick={() => state.toggleOpen()}>
|
||||
<span class="absolute i-[tabler--chevron-left] w-6 h-6 block"></span>
|
||||
</button>
|
||||
{#each keys as panel (panels[panel].id)}
|
||||
{#if panels[panel].visible !== false}
|
||||
{#each state.keys as panelId (panelId)}
|
||||
{#if !state.panels[panelId].hidden}
|
||||
<button
|
||||
aria-label={panel}
|
||||
class="tab {panels[panel].classes}"
|
||||
class:active={panel === $activePanel}
|
||||
on:click={() => setActivePanel(panel)}
|
||||
aria-label={panelId}
|
||||
class="tab {state.panels[panelId].classes}"
|
||||
class:active={panelId === state.activePanel.value}
|
||||
onclick={() => (state.activePanel.value = panelId)}
|
||||
>
|
||||
<span class={`block w-6 h-6 ${panels[panel].icon}`}></span>
|
||||
<span class={`block w-6 h-6 iconify ${state.panels[panelId].icon}`}></span>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,15 +38,13 @@
|
||||
height: 100%;
|
||||
right: 0px;
|
||||
transform: translateX(calc(100% - 30px));
|
||||
transition:
|
||||
transform 0.2s,
|
||||
background 0.2s ease;
|
||||
transition: transform 0.2s, background 0.2s ease;
|
||||
width: 30%;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--layer-1);
|
||||
background: var(--color-layer-1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
@@ -103,7 +54,7 @@
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: solid thin var(--outline);
|
||||
border-right: solid thin var(--color-outline);
|
||||
}
|
||||
|
||||
.tabs > button {
|
||||
@@ -114,9 +65,9 @@
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-left: solid thin var(--outline);
|
||||
background: var(--layer-1);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
border-left: solid thin var(--color-outline);
|
||||
background: var(--color-layer-1);
|
||||
}
|
||||
|
||||
.tabs > button > span {
|
||||
@@ -124,7 +75,7 @@
|
||||
}
|
||||
|
||||
.tabs > button.active {
|
||||
background: var(--layer-2);
|
||||
background: var(--color-layer-2);
|
||||
}
|
||||
|
||||
.tabs > button.active span {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user