Compare commits
84 Commits
feat/arena
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9271d3a7e4
|
||
|
|
e44b73bebf
|
||
|
979e9fd922
|
|||
|
544500e7fe
|
|||
|
aaebbc4bc0
|
|||
|
|
894ab70b79 | ||
|
f8a2a95bc1
|
|||
|
c9dd143916
|
|||
|
898dd49aee
|
|||
|
9fb69d760f
|
|||
|
bafbcca2b8
|
|||
|
8ad9e5535c
|
|||
|
|
43a3c54838 | ||
|
11eaeb719b
|
|||
|
74c2978cd1
|
|||
|
4fdc247904
|
|||
|
c3f8b4b5aa
|
|||
|
67591c0572
|
|||
|
de1f9d6ab6
|
|||
|
6acce72fb8
|
|||
|
cf8943b205
|
|||
|
9e03d36482
|
|||
|
fd7268d620
|
|||
|
6358c22a85
|
|||
|
655b6a18b2
|
|||
|
37b2bdc8bd
|
|||
|
94e01d4ea8
|
|||
|
35f5177884
|
|||
|
ac2c61f221
|
|||
|
ef3d46279f
|
|||
|
703da324fa
|
|||
|
1dae472253
|
|||
|
09fdfb88cd
|
|||
|
04b63cc7e2
|
|||
|
cb6a35606d
|
|||
|
9c9f3ba3b7
|
|||
|
08dda2b2cb
|
|||
|
059129a738
|
|||
|
437c9f4a25
|
|||
|
48bf447ce1
|
|||
|
548fa4f0a1
|
|||
|
|
642cca30ad | ||
|
|
419249aca3 | ||
|
c69cb94ac7
|
|||
|
|
4b652d885f | ||
|
381f784775
|
|||
| 91866b4e9a | |||
|
01f1568221
|
|||
|
3e8d2768b3
|
|||
|
16a832779a
|
|||
|
d582915842
|
|||
|
|
caaecd7a02 | ||
|
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
|
@@ -1,9 +0,0 @@
|
|||||||
[target.wasm32-unknown-unknown]
|
|
||||||
rustflags = [
|
|
||||||
"-C",
|
|
||||||
"link-arg=--import-memory",
|
|
||||||
"-C",
|
|
||||||
"link-arg=--initial-memory=67108864", # 64 MiB
|
|
||||||
"-C",
|
|
||||||
"link-arg=--max-memory=536870912", # 512 MiB
|
|
||||||
]
|
|
||||||
@@ -10,38 +10,29 @@
|
|||||||
"json": {
|
"json": {
|
||||||
// https://dprint.dev/plugins/json/config/
|
// https://dprint.dev/plugins/json/config/
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {},
|
||||||
},
|
"toml": {},
|
||||||
"toml": {
|
"dockerfile": {},
|
||||||
},
|
|
||||||
"dockerfile": {
|
|
||||||
},
|
|
||||||
"ruff": {
|
|
||||||
},
|
|
||||||
"jupyter": {
|
|
||||||
},
|
|
||||||
"malva": {
|
|
||||||
},
|
|
||||||
"markup": {
|
"markup": {
|
||||||
// https://dprint.dev/plugins/markup_fmt/config/
|
// https://dprint.dev/plugins/markup_fmt/config/
|
||||||
"scriptIndent": true,
|
"scriptIndent": true,
|
||||||
"styleIndent": true,
|
"styleIndent": true,
|
||||||
},
|
},
|
||||||
"yaml": {
|
"yaml": {},
|
||||||
},
|
|
||||||
"graphql": {
|
|
||||||
},
|
|
||||||
"exec": {
|
"exec": {
|
||||||
"cwd": "${configDir}",
|
"cwd": "${configDir}",
|
||||||
|
"commands": [
|
||||||
"commands": [{
|
{
|
||||||
"command": "rustfmt",
|
"command": "rustfmt",
|
||||||
"exts": ["rs"],
|
"exts": [
|
||||||
"cacheKeyFiles": [
|
"rs",
|
||||||
"rustfmt.toml",
|
],
|
||||||
"rust-toolchain.toml",
|
"cacheKeyFiles": [
|
||||||
],
|
"rustfmt.toml",
|
||||||
}],
|
"rust-toolchain.toml",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"excludes": [
|
"excludes": [
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
@@ -51,6 +42,9 @@
|
|||||||
"**/*-lock.yaml",
|
"**/*-lock.yaml",
|
||||||
"**/yaml.lock",
|
"**/yaml.lock",
|
||||||
"**/.DS_Store",
|
"**/.DS_Store",
|
||||||
|
"**/.pnpm-store",
|
||||||
|
"**/.cargo",
|
||||||
|
"**/target",
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
|
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
|
||||||
@@ -58,13 +52,8 @@
|
|||||||
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
|
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
|
||||||
"https://plugins.dprint.dev/toml-0.7.0.wasm",
|
"https://plugins.dprint.dev/toml-0.7.0.wasm",
|
||||||
"https://plugins.dprint.dev/dockerfile-0.3.3.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/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_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/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 |
43
.gitea/scripts/build.sh
Executable file
43
.gitea/scripts/build.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p app/static
|
||||||
|
|
||||||
|
cp CHANGELOG.md app/static/CHANGELOG.md
|
||||||
|
|
||||||
|
# Derive branch/tag info
|
||||||
|
REF_TYPE="${GITHUB_REF_TYPE:-branch}"
|
||||||
|
REF_NAME="${GITHUB_REF_NAME:-$(basename "$GITHUB_REF")}"
|
||||||
|
BRANCH="${GITHUB_HEAD_REF:-}"
|
||||||
|
if [[ -z "$BRANCH" && "$REF_TYPE" == "branch" ]]; then
|
||||||
|
BRANCH="$REF_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine last tag and commits since
|
||||||
|
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
||||||
|
if [[ -n "$LAST_TAG" ]]; then
|
||||||
|
COMMITS_SINCE_LAST_RELEASE="$(git rev-list --count "${LAST_TAG}..HEAD")"
|
||||||
|
else
|
||||||
|
COMMITS_SINCE_LAST_RELEASE="0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >app/static/git.json <<EOF
|
||||||
|
{
|
||||||
|
"ref": "${GITHUB_REF:-}",
|
||||||
|
"ref_name": "${REF_NAME}",
|
||||||
|
"ref_type": "${REF_TYPE}",
|
||||||
|
"sha": "${GITHUB_SHA:-}",
|
||||||
|
"run_number": "${GITHUB_RUN_NUMBER:-}",
|
||||||
|
"event_name": "${GITHUB_EVENT_NAME:-}",
|
||||||
|
"workflow": "${GITHUB_WORKFLOW:-}",
|
||||||
|
"job": "${GITHUB_JOB:-}",
|
||||||
|
"commit_message": "$(git log -1 --pretty=%B)",
|
||||||
|
"commit_timestamp": "$(git log -1 --pretty=%cI)",
|
||||||
|
"branch": "${BRANCH}",
|
||||||
|
"commits_since_last_release": "${COMMITS_SINCE_LAST_RELEASE}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
cp -R packages/ui/build app/build/ui
|
||||||
18
.gitea/scripts/ci-checks.sh
Executable file
18
.gitea/scripts/ci-checks.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
pnpm lint &
|
||||||
|
LINT_PID=$!
|
||||||
|
pnpm format:check &
|
||||||
|
FORMAT_PID=$!
|
||||||
|
pnpm check &
|
||||||
|
TYPE_PID=$!
|
||||||
|
xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" pnpm test &
|
||||||
|
TEST_PID=$!
|
||||||
|
|
||||||
|
wait $LINT_PID
|
||||||
|
wait $FORMAT_PID
|
||||||
|
wait $TYPE_PID
|
||||||
|
wait $TEST_PID
|
||||||
91
.gitea/scripts/create-release.sh
Executable file
91
.gitea/scripts/create-release.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
TAG="$GITHUB_REF_NAME"
|
||||||
|
VERSION=$(echo "$TAG" | sed 's/^v//')
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
echo "🚀 Creating release for $TAG"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 1. Extract release notes from annotated tag
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Ensure the local git knows this is an annotated tag with metadata
|
||||||
|
git fetch origin "refs/tags/$TAG:refs/tags/$TAG" --force
|
||||||
|
|
||||||
|
# %(contents) gets the whole message.
|
||||||
|
# If you want ONLY what you typed after the first line, use %(contents:body)
|
||||||
|
NOTES=$(git tag -l "$TAG" --format='%(contents)' | sed '/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----/d')
|
||||||
|
|
||||||
|
if [ -z "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
|
||||||
|
echo "❌ Tag message is empty or tag is not annotated"
|
||||||
|
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 -r file; do
|
||||||
|
tmp_file="$file.tmp"
|
||||||
|
jq --arg v "$VERSION" '.version = $v' "$file" >"$tmp_file"
|
||||||
|
mv "$tmp_file" "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 3. Generate commit list since last release
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
LAST_TAG=$(git tag --sort=-creatordate | grep -v "^$TAG$" | head -n 1 || echo "")
|
||||||
|
|
||||||
|
if [ -n "$LAST_TAG" ]; then
|
||||||
|
# Filter out previous 'chore(release)' commits so the list stays clean
|
||||||
|
COMMITS=$(git log "$LAST_TAG"..HEAD --pretty=format:'* [%h](https://git.max-richter.dev/max/nodarium/commit/%H) %s' | grep -v "chore(release)")
|
||||||
|
else
|
||||||
|
COMMITS=$(git log HEAD --pretty=format:'* [%h](https://git.max-richter.dev/max/nodarium/commit/%H) %s' | grep -v "chore(release)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 4. Update CHANGELOG.md (prepend)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
tmp_changelog="CHANGELOG.tmp"
|
||||||
|
{
|
||||||
|
echo "# $TAG ($DATE)"
|
||||||
|
echo ""
|
||||||
|
echo "$NOTES"
|
||||||
|
echo ""
|
||||||
|
if [ -n "$COMMITS" ]; then
|
||||||
|
echo "---"
|
||||||
|
echo "$COMMITS"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
if [ -f CHANGELOG.md ]; then
|
||||||
|
cat CHANGELOG.md
|
||||||
|
fi
|
||||||
|
} >"$tmp_changelog"
|
||||||
|
|
||||||
|
mv "$tmp_changelog" CHANGELOG.md
|
||||||
|
|
||||||
|
pnpm exec dprint fmt CHANGELOG.md
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 5. 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/*")
|
||||||
|
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit for release $TAG"
|
||||||
|
else
|
||||||
|
git commit -m "chore(release): $TAG"
|
||||||
|
git push origin main
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm app/static/CHANGELOG.md
|
||||||
|
cp CHANGELOG.md app/static/CHANGELOG.md
|
||||||
|
echo "✅ Release process for $TAG complete"
|
||||||
43
.gitea/scripts/deploy-files.sh
Executable file
43
.gitea/scripts/deploy-files.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Configuring rclone"
|
||||||
|
|
||||||
|
KEY_FILE="$(mktemp)"
|
||||||
|
echo "${SSH_PRIVATE_KEY}" >"${KEY_FILE}"
|
||||||
|
chmod 600 "${KEY_FILE}"
|
||||||
|
|
||||||
|
mkdir -p ~/.config/rclone
|
||||||
|
cat >~/.config/rclone/rclone.conf <<EOF
|
||||||
|
[sftp-remote]
|
||||||
|
type = sftp
|
||||||
|
host = ${SSH_HOST}
|
||||||
|
user = ${SSH_USER}
|
||||||
|
port = ${SSH_PORT}
|
||||||
|
key_file = ${KEY_FILE}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
|
||||||
|
TARGET_DIR="${REMOTE_DIR}"
|
||||||
|
elif [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then
|
||||||
|
SAFE_PR_NAME="${GITHUB_HEAD_REF//\//-}"
|
||||||
|
TARGET_DIR="${REMOTE_DIR}_${SAFE_PR_NAME}"
|
||||||
|
elif [[ "${GITHUB_REF_NAME:-}" == "main" ]]; then
|
||||||
|
TARGET_DIR="${REMOTE_DIR}_main"
|
||||||
|
else
|
||||||
|
SAFE_REF="${GITHUB_REF_NAME//\//-}"
|
||||||
|
TARGET_DIR="${REMOTE_DIR}_${SAFE_REF}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying to ${TARGET_DIR}"
|
||||||
|
|
||||||
|
rclone sync \
|
||||||
|
--update \
|
||||||
|
--verbose \
|
||||||
|
--progress \
|
||||||
|
--exclude _astro/** \
|
||||||
|
--stats 2s \
|
||||||
|
--stats-one-line \
|
||||||
|
--transfers 4 \
|
||||||
|
./app/build/ \
|
||||||
|
"sftp-remote:${TARGET_DIR}"
|
||||||
41
.gitea/workflows/build-ci-image.yaml
Normal file
41
.gitea/workflows/build-ci-image.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Build & Push CI Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "Dockerfile.ci"
|
||||||
|
- ".gitea/workflows/build-ci-image.yaml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
config-inline: |
|
||||||
|
[registry."git.max-richter.dev"]
|
||||||
|
https = true
|
||||||
|
insecure = false
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.max-richter.dev
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.ci
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.max-richter.dev/${{ gitea.repository }}-ci:latest
|
||||||
|
git.max-richter.dev/${{ gitea.repository }}-ci:${{ gitea.sha }}
|
||||||
76
.gitea/workflows/release.yaml
Normal file
76
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: 🚀 Lint & Test & Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["*"]
|
||||||
|
tags: ["*"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["*"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
PNPM_CACHE_FOLDER: .pnpm-store
|
||||||
|
CARGO_HOME: .cargo
|
||||||
|
CARGO_TARGET_DIR: target
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: git.max-richter.dev/max/nodarium-ci:fd7268d6208aede435e1685817ae6b271c68bd83
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📑 Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITEA_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: 🦀 Cache Cargo
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: 📦 Install Dependencies
|
||||||
|
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
|
||||||
|
|
||||||
|
- name: 🧹 Quality Control
|
||||||
|
run: ./.gitea/scripts/ci-checks.sh
|
||||||
|
|
||||||
|
- name: 🛠️ Build
|
||||||
|
run: ./.gitea/scripts/build.sh
|
||||||
|
|
||||||
|
- name: 🚀 Create Release Commit
|
||||||
|
if: gitea.ref_type == 'tag'
|
||||||
|
run: ./.gitea/scripts/create-release.sh
|
||||||
|
|
||||||
|
- name: 🏷️ Create Gitea Release
|
||||||
|
if: gitea.ref_type == 'tag'
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ gitea.ref_name }}
|
||||||
|
release_name: Release ${{ gitea.ref_name }}
|
||||||
|
body_path: CHANGELOG.md
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: 🚀 Deploy Changed Files via rclone
|
||||||
|
run: ./.gitea/scripts/deploy-files.sh
|
||||||
|
env:
|
||||||
|
REMOTE_DIR: ${{ vars.REMOTE_DIR }}
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||||
|
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||||
|
SSH_USER: ${{ vars.SSH_USER }}
|
||||||
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 }}
|
|
||||||
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# v0.0.3 (2026-02-07)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Edge dragging now highlights valid connection sockets, improving graph editing clarity.
|
||||||
|
- InputNumber supports snapping to predefined values while holding Alt.
|
||||||
|
- Changelog is accessible directly from the sidebar and now includes git metadata and a list of commits.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fixed incorrect socket highlighting when an edge already existed.
|
||||||
|
- Corrected initialization of `InputNumber` values outside min/max bounds.
|
||||||
|
- Fixed initialization of nested vec3 inputs.
|
||||||
|
- Multiple CI fixes to ensure reliable builds, correct environment variables, and proper image handling.
|
||||||
|
|
||||||
|
## Maintenance / CI
|
||||||
|
|
||||||
|
- Significant CI and Dockerfile cleanup and optimization.
|
||||||
|
- Improved git metadata generation during builds.
|
||||||
|
- Dependency updates, formatting, and test snapshot updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [f8a2a95](https://git.max-richter.dev/max/nodarium/commit/f8a2a95bc18fa3c8c1db67dc0c2b66db1ff0d866) chore: clean CHANGELOG.md
|
||||||
|
- [c9dd143](https://git.max-richter.dev/max/nodarium/commit/c9dd143916d758991f3ba30723a32c18b6f98bb5) fix(ci): correctly add release notes from tag to changelog
|
||||||
|
- [898dd49](https://git.max-richter.dev/max/nodarium/commit/898dd49aee930350af8645382ef5042765a1fac7) fix(ci): correctly copy changelog to build output
|
||||||
|
- [9fb69d7](https://git.max-richter.dev/max/nodarium/commit/9fb69d760fdf92ecc2448e468242970ec48443b0) feat: show commits since last release in changelog
|
||||||
|
- [bafbcca](https://git.max-richter.dev/max/nodarium/commit/bafbcca2b8a7cd9f76e961349f11ec84d1e4da63) fix: wrong socket was highlighted when dragging node
|
||||||
|
- [8ad9e55](https://git.max-richter.dev/max/nodarium/commit/8ad9e5535cd752ef111504226b4dac57b5adcf3d) feat: highlight possible sockets when dragging edge
|
||||||
|
- [11eaeb7](https://git.max-richter.dev/max/nodarium/commit/11eaeb719be7f34af8db8b7908008a15308c0cac) feat(app): display some git metadata in changelog
|
||||||
|
- [74c2978](https://git.max-richter.dev/max/nodarium/commit/74c2978cd16d2dd95ce1ae8019dfb9098e52b4b6) chore: cleanup git.json a bit
|
||||||
|
- [4fdc247](https://git.max-richter.dev/max/nodarium/commit/4fdc24790490d3f13ee94a557159617f4077a2f9) ci: update build.sh to correct git.json
|
||||||
|
- [c3f8b4b](https://git.max-richter.dev/max/nodarium/commit/c3f8b4b5aad7a525fb11ab14c9236374cb60442d) ci: debug available env vars
|
||||||
|
- [67591c0](https://git.max-richter.dev/max/nodarium/commit/67591c0572b873d8c7cd00db8efb7dac2d6d4de2) chore: pnpm format
|
||||||
|
- [de1f9d6](https://git.max-richter.dev/max/nodarium/commit/de1f9d6ab669b8e699d98b8855e125e21030b5b3) feat(ui): change inputnumber to snap to values when alt is pressed
|
||||||
|
- [6acce72](https://git.max-richter.dev/max/nodarium/commit/6acce72fb8c416cc7f6eec99c2ae94d6529e960c) fix(ui): correctly initialize InputNumber
|
||||||
|
- [cf8943b](https://git.max-richter.dev/max/nodarium/commit/cf8943b2059aa286e41865caf75058d35498daf7) chore: pnpm update
|
||||||
|
- [9e03d36](https://git.max-richter.dev/max/nodarium/commit/9e03d36482bb4f972c384b66b2dcf258f0cd18be) chore: use newest ci image
|
||||||
|
- [fd7268d](https://git.max-richter.dev/max/nodarium/commit/fd7268d6208aede435e1685817ae6b271c68bd83) ci: make dockerfile work
|
||||||
|
- [6358c22](https://git.max-richter.dev/max/nodarium/commit/6358c22a853ec340be5223fabb8289092e4f4afe) ci: use tagged own image for ci
|
||||||
|
- [655b6a1](https://git.max-richter.dev/max/nodarium/commit/655b6a18b282f0cddcc750892e575ee6c311036b) ci: make dockerfile work
|
||||||
|
- [37b2bdc](https://git.max-richter.dev/max/nodarium/commit/37b2bdc8bdbd8ded6b22b89214b49de46f788351) ci: update ci Dockerfile to work
|
||||||
|
- [94e01d4](https://git.max-richter.dev/max/nodarium/commit/94e01d4ea865f15ce06b52827a1ae6906de5be5e) ci: correctly build and push ci image
|
||||||
|
- [35f5177](https://git.max-richter.dev/max/nodarium/commit/35f5177884b62bbf119af1bbf4df61dd0291effb) feat: try to optimize the Dockerfile
|
||||||
|
- [ac2c61f](https://git.max-richter.dev/max/nodarium/commit/ac2c61f2211ba96bbdbb542179905ca776537cec) ci: use actual git url in ci
|
||||||
|
- [ef3d462](https://git.max-richter.dev/max/nodarium/commit/ef3d46279f4ff9c04d80bb2d9a9e7cfec63b224e) fix(ci): build before testing
|
||||||
|
- [703da32](https://git.max-richter.dev/max/nodarium/commit/703da324fabbef0e2c017f0f7a925209fa26bd03) ci: automatically build ci image and store locally
|
||||||
|
- [1dae472](https://git.max-richter.dev/max/nodarium/commit/1dae472253ccb5e3766f2270adc053b922f46738) ci: add a git.json metadata file during build
|
||||||
|
- [09fdfb8](https://git.max-richter.dev/max/nodarium/commit/09fdfb88cd203ace0e36663ebdb2c8c7ba53f190) chore: update test screenshots
|
||||||
|
- [04b63cc](https://git.max-richter.dev/max/nodarium/commit/04b63cc7e2fc4fcfa0973cf40592d11457179db3) feat: add changelog to sidebar
|
||||||
|
- [cb6a356](https://git.max-richter.dev/max/nodarium/commit/cb6a35606dfda50b0c81b04902d7a6c8e59458d2) feat(ci): also cache cargo stuff
|
||||||
|
- [9c9f3ba](https://git.max-richter.dev/max/nodarium/commit/9c9f3ba3b7c94215a86b0a338a5cecdd87b96b28) fix(ci): use GITHUB_instead of GITEA_ for env vars
|
||||||
|
- [08dda2b](https://git.max-richter.dev/max/nodarium/commit/08dda2b2cb4d276846abe30bc260127626bb508a) chore: pnpm format
|
||||||
|
- [059129a](https://git.max-richter.dev/max/nodarium/commit/059129a738d02b8b313bb301a515697c7c4315ac) fix(ci): deploy prs and main
|
||||||
|
- [437c9f4](https://git.max-richter.dev/max/nodarium/commit/437c9f4a252125e1724686edace0f5f006f58439) feat(ci): add list of all commits to changelog entry
|
||||||
|
- [48bf447](https://git.max-richter.dev/max/nodarium/commit/48bf447ce12949d7c29a230806d160840b7847e1) docs: straighten up changelog a bit
|
||||||
|
- [548fa4f](https://git.max-richter.dev/max/nodarium/commit/548fa4f0a1a14adc40a74da1182fa6da81eab3df) fix(app): correctly initialize vec3 inputs in nestedsettings
|
||||||
|
|
||||||
|
# v0.0.2 (2026-02-04)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- []() fix(ci): actually deploy on tags
|
||||||
|
- []() fix(app): correctly handle false value in settings
|
||||||
|
|
||||||
|
# v0.0.1 (2026-02-03)
|
||||||
|
|
||||||
|
chore: format
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -24,14 +24,6 @@ dependencies = [
|
|||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "debug"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"nodarium_macros",
|
|
||||||
"nodarium_utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "float"
|
name = "float"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -1,15 +0,0 @@
|
|||||||
FROM node:24-alpine
|
|
||||||
|
|
||||||
RUN apk add --no-cache --update curl rclone g++
|
|
||||||
|
|
||||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
|
||||||
CARGO_HOME=/usr/local/cargo \
|
|
||||||
PATH=/usr/local/cargo/bin:$PATH
|
|
||||||
|
|
||||||
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 target add wasm32-unknown-unknown \
|
|
||||||
&& npm i -g pnpm
|
|
||||||
30
Dockerfile.ci
Normal file
30
Dockerfile.ci
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM node:25-bookworm-slim
|
||||||
|
|
||||||
|
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||||
|
CARGO_HOME=/usr/local/cargo \
|
||||||
|
PATH=/usr/local/cargo/bin:$PATH
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates=20230311+deb12u1 \
|
||||||
|
curl=7.88.1-10+deb12u14 \
|
||||||
|
git=1:2.39.5-0+deb12u3 \
|
||||||
|
jq=1.6-2.1+deb12u1 \
|
||||||
|
g++=4:12.2.0-3 \
|
||||||
|
rclone=1.60.1+dfsg-2+b5 \
|
||||||
|
xvfb=2:21.1.7-3+deb12u11 \
|
||||||
|
xauth=1:1.1.2-1 \
|
||||||
|
--no-install-recommends \
|
||||||
|
# Install Rust
|
||||||
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||||
|
--default-toolchain stable \
|
||||||
|
--profile minimal \
|
||||||
|
&& rustup target add wasm32-unknown-unknown \
|
||||||
|
# Setup Playwright
|
||||||
|
&& npm i -g pnpm \
|
||||||
|
&& pnpm dlx playwright install --with-deps firefox \
|
||||||
|
# Final Cleanup
|
||||||
|
&& rm -rf /usr/local/rustup/downloads /usr/local/rustup/tmp \
|
||||||
|
&& rm -rf /usr/local/cargo/registry/index /usr/local/cargo/registry/cache \
|
||||||
|
&& rm -rf /usr/local/rustup/toolchains/*/share/doc \
|
||||||
|
&& apt-get purge -y --auto-remove \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
30
README.md
30
README.md
@@ -4,11 +4,11 @@ Nodarium
|
|||||||
|
|
||||||
<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">
|
<p align="center">
|
||||||
Nodarium is a WebAssembly based visual programming language.
|
Nodarium is a WebAssembly based visual programming language.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<img src=".github/graphics/nodes.svg" width="80%"/>
|
<img src=".gitea/graphics/nodes.svg" width="80%"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ Currently this visual programming language is used to develop <https://nodes.max
|
|||||||
- [Node.js](https://nodejs.org/en/download)
|
- [Node.js](https://nodejs.org/en/download)
|
||||||
- [pnpm](https://pnpm.io/installation)
|
- [pnpm](https://pnpm.io/installation)
|
||||||
- [rust](https://www.rust-lang.org/tools/install)
|
- [rust](https://www.rust-lang.org/tools/install)
|
||||||
|
- wasm-pack
|
||||||
|
|
||||||
### Install dependencies
|
### Install dependencies
|
||||||
|
|
||||||
@@ -49,4 +50,29 @@ pnpm dev
|
|||||||
|
|
||||||
### [Now you can create your first node 🤓](./docs/DEVELOPING_NODES.md)
|
### [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
|
# Roadmap
|
||||||
|
|||||||
@@ -1,783 +0,0 @@
|
|||||||
# Shared Memory Refactor Plan
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
Migrate to a single shared `WebAssembly.Memory` instance imported by all nodes using `--import-memory`. The `#[nodarium_execute]` macro writes the function's return value directly to shared memory at the specified offset.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Shared WebAssembly.Memory │
|
|
||||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ [Node A output] [Node B output] [Node C output] ... │ │
|
|
||||||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
|
||||||
│ │ │ Vec<i32> │ │ Vec<i32> │ │ Vec<i32> │ │ │
|
|
||||||
│ │ │ 4 bytes │ │ 12 bytes │ │ 2KB │ │ │
|
|
||||||
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ offset: 0 ────────────────────────────────────────────────► │ │
|
|
||||||
│ └───────────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
|
||||||
▲
|
|
||||||
│
|
|
||||||
│ import { memory } from "env"
|
|
||||||
┌─────────────────────────┼─────────────────────────┐
|
|
||||||
│ │ │
|
|
||||||
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
|
|
||||||
│ Node A │ │ Node B │ │ Node C │
|
|
||||||
│ WASM │ │ WASM │ │ WASM │
|
|
||||||
└─────────┘ └─────────┘ └─────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 1: Compilation Configuration
|
|
||||||
|
|
||||||
### 1.1 Cargo Config
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# nodes/max/plantarium/box/.cargo/config.toml
|
|
||||||
[build]
|
|
||||||
rustflags = ["-C", "link-arg=--import-memory"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Or globally in `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[profile.release]
|
|
||||||
rustflags = ["-C", "link-arg=--import-memory"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Import Memory Semantics
|
|
||||||
|
|
||||||
With `--import-memory`:
|
|
||||||
|
|
||||||
- Nodes **import** memory from the host (not export their own)
|
|
||||||
- All nodes receive the same `WebAssembly.Memory` instance
|
|
||||||
- Memory is read/write accessible from all modules
|
|
||||||
- No `memory.grow` needed (host manages allocation)
|
|
||||||
|
|
||||||
## Phase 2: Macro Design
|
|
||||||
|
|
||||||
### 2.1 Clean Node API
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// input.json has 3 inputs: op_type, a, b
|
|
||||||
nodarium_definition_file!("src/input.json");
|
|
||||||
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(op_type: *i32, a: *i32, b: *i32) -> Vec<i32> {
|
|
||||||
// Read inputs directly from shared memory
|
|
||||||
let op = unsafe { *op_type };
|
|
||||||
let a_val = f32::from_bits(unsafe { *a } as u32);
|
|
||||||
let b_val = f32::from_bits(unsafe { *b } as u32);
|
|
||||||
|
|
||||||
let result = match op {
|
|
||||||
0 => a_val + b_val,
|
|
||||||
1 => a_val - b_val,
|
|
||||||
2 => a_val * b_val,
|
|
||||||
3 => a_val / b_val,
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return Vec<i32>, macro handles writing to shared memory
|
|
||||||
vec![result.to_bits()]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Macro Implementation
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// packages/macros/src/lib.rs
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn nodarium_execute(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
let input_fn = parse_macro_input!(item as syn::ItemFn);
|
|
||||||
let fn_name = &input_fn.sig.ident;
|
|
||||||
|
|
||||||
// Parse definition to get input count
|
|
||||||
let project_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
||||||
let def: NodeDefinition = serde_json::from_str(&fs::read_to_string(
|
|
||||||
Path::new(&project_dir).join("src/input.json")
|
|
||||||
).unwrap()).unwrap();
|
|
||||||
|
|
||||||
let input_count = def.inputs.as_ref().map(|i| i.len()).unwrap_or(0);
|
|
||||||
|
|
||||||
// Validate signature
|
|
||||||
validate_signature(&input_fn, input_count);
|
|
||||||
|
|
||||||
// Generate wrapper
|
|
||||||
generate_execute_wrapper(input_fn, fn_name, input_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_signature(fn_sig: &syn::Signature, expected_inputs: usize) {
|
|
||||||
let param_count = fn_sig.inputs.len();
|
|
||||||
if param_count != expected_inputs {
|
|
||||||
panic!(
|
|
||||||
"Execute function has {} parameters but definition has {} inputs\n\
|
|
||||||
Definition inputs: {:?}\n\
|
|
||||||
Expected signature:\n\
|
|
||||||
pub fn execute({}) -> Vec<i32>",
|
|
||||||
param_count,
|
|
||||||
expected_inputs,
|
|
||||||
def.inputs.as_ref().map(|i| i.keys().collect::<Vec<_>>()),
|
|
||||||
(0..expected_inputs)
|
|
||||||
.map(|i| format!("arg{}: *const i32", i))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify return type is Vec<i32>
|
|
||||||
match &fn_sig.output {
|
|
||||||
syn::ReturnType::Type(_, ty) => {
|
|
||||||
if !matches!(&**ty, syn::Type::Path(tp) if tp.path.is_ident("Vec")) {
|
|
||||||
panic!("Execute function must return Vec<i32>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
syn::ReturnType::Default => {
|
|
||||||
panic!("Execute function must return Vec<i32>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_execute_wrapper(
|
|
||||||
input_fn: syn::ItemFn,
|
|
||||||
fn_name: &syn::Ident,
|
|
||||||
input_count: usize,
|
|
||||||
) -> TokenStream {
|
|
||||||
let arg_names: Vec<_> = (0..input_count)
|
|
||||||
.map(|i| syn::Ident::new(&format!("arg{}", i), proc_macro2::Span::call_site()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let expanded = quote! {
|
|
||||||
#input_fn
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn execute(
|
|
||||||
output_pos: i32,
|
|
||||||
#( #arg_names: i32 ),*
|
|
||||||
) -> i32 {
|
|
||||||
extern "C" {
|
|
||||||
fn __nodarium_log(ptr: *const u8, len: usize);
|
|
||||||
fn __nodarium_log_panic(ptr: *const u8, len: usize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup panic hook
|
|
||||||
static SET_HOOK: std::sync::Once = std::sync::Once::new();
|
|
||||||
SET_HOOK.call_once(|| {
|
|
||||||
std::panic::set_hook(Box::new(|info| {
|
|
||||||
let msg = info.to_string();
|
|
||||||
unsafe { __nodarium_log_panic(msg.as_ptr(), msg.len()); }
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call user function
|
|
||||||
let result = #fn_name(
|
|
||||||
#( #arg_names as *const i32 ),*
|
|
||||||
);
|
|
||||||
|
|
||||||
// Write result directly to shared memory at output_pos
|
|
||||||
let len_bytes = result.len() * 4;
|
|
||||||
unsafe {
|
|
||||||
let src = result.as_ptr() as *const u8;
|
|
||||||
let dst = output_pos as *mut u8;
|
|
||||||
dst.copy_from_nonoverlapping(src, len_bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forget the Vec to prevent deallocation (data is in shared memory now)
|
|
||||||
core::mem::forget(result);
|
|
||||||
|
|
||||||
len_bytes as i32
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TokenStream::from(expanded)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 Generated Assembly
|
|
||||||
|
|
||||||
The macro generates:
|
|
||||||
|
|
||||||
```asm
|
|
||||||
; Input: output_pos in register r0, arg0 in r1, arg1 in r2, arg2 in r3
|
|
||||||
execute:
|
|
||||||
; Call user function
|
|
||||||
bl user_execute ; returns pointer to Vec<i32> in r0
|
|
||||||
|
|
||||||
; Calculate byte length
|
|
||||||
ldr r4, [r0, #8] ; Vec::len field
|
|
||||||
lsl r4, r4, #2 ; len * 4 (i32 = 4 bytes)
|
|
||||||
|
|
||||||
; Copy Vec data to shared memory at output_pos
|
|
||||||
ldr r5, [r0, #0] ; Vec::ptr field
|
|
||||||
ldr r6, [r0, #4] ; capacity (unused)
|
|
||||||
|
|
||||||
; memcpy(dst=output_pos, src=r5, len=r4)
|
|
||||||
; (implemented via copy_from_nonoverlapping)
|
|
||||||
|
|
||||||
; Return length
|
|
||||||
mov r0, r4
|
|
||||||
bx lr
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 3: Input Reading Helpers
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// packages/utils/src/accessor.rs
|
|
||||||
|
|
||||||
/// Read i32 from shared memory
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_i32(ptr: *const i32) -> i32 {
|
|
||||||
*ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read f32 from shared memory (stored as i32 bits)
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_f32(ptr: *const i32) -> f32 {
|
|
||||||
f32::from_bits(*ptr as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read boolean from shared memory
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_bool(ptr: *const i32) -> bool {
|
|
||||||
*ptr != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read vec3 (3 f32s) from shared memory
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_vec3(ptr: *const i32) -> [f32; 3] {
|
|
||||||
let p = ptr as *const f32;
|
|
||||||
[p.read(), p.add(1).read(), p.add(2).read()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read slice from shared memory
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_i32_slice(ptr: *const i32, len: usize) -> &[i32] {
|
|
||||||
std::slice::from_raw_parts(ptr, len)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read f32 slice from shared memory
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_f32_slice(ptr: *const i32, len: usize) -> &[f32] {
|
|
||||||
std::slice::from_raw_parts(ptr as *const f32, len)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read with default value
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_f32_default(ptr: *const i32, default: f32) -> f32 {
|
|
||||||
if ptr.is_null() { default } else { read_f32(ptr) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub unsafe fn read_i32_default(ptr: *const i32, default: i32) -> i32 {
|
|
||||||
if ptr.is_null() { default } else { read_i32(ptr) }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 4: Node Implementation Examples
|
|
||||||
|
|
||||||
### 4.1 Math Node
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// nodes/max/plantarium/math/src/lib.rs
|
|
||||||
|
|
||||||
nodarium_definition_file!("src/input.json");
|
|
||||||
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(op_type: *const i32, a: *const i32, b: *const i32) -> Vec<i32> {
|
|
||||||
use nodarium_utils::{read_i32, read_f32};
|
|
||||||
|
|
||||||
let op = unsafe { read_i32(op_type) };
|
|
||||||
let a_val = unsafe { read_f32(a) };
|
|
||||||
let b_val = unsafe { read_f32(b) };
|
|
||||||
|
|
||||||
let result = match op {
|
|
||||||
0 => a_val + b_val, // add
|
|
||||||
1 => a_val - b_val, // subtract
|
|
||||||
2 => a_val * b_val, // multiply
|
|
||||||
3 => a_val / b_val, // divide
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
vec![result.to_bits()]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Vec3 Node
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// nodes/max/plantarium/vec3/src/lib.rs
|
|
||||||
|
|
||||||
nodarium_definition_file!("src/input.json");
|
|
||||||
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(x: *const i32, y: *const i32, z: *const i32) -> Vec<i32> {
|
|
||||||
use nodarium_utils::read_f32;
|
|
||||||
|
|
||||||
let x_val = unsafe { read_f32(x) };
|
|
||||||
let y_val = unsafe { read_f32(y) };
|
|
||||||
let z_val = unsafe { read_f32(z) };
|
|
||||||
|
|
||||||
vec![x_val.to_bits(), y_val.to_bits(), z_val.to_bits()]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Box Node
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// nodes/max/plantarium/box/src/lib.rs
|
|
||||||
|
|
||||||
nodarium_definition_file!("src/input.json");
|
|
||||||
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(size: *const i32) -> Vec<i32> {
|
|
||||||
use nodarium_utils::{read_f32, encode_float, calculate_normals};
|
|
||||||
|
|
||||||
let size = unsafe { read_f32(size) };
|
|
||||||
let p = encode_float(size);
|
|
||||||
let n = encode_float(-size);
|
|
||||||
|
|
||||||
let mut cube_geometry = vec![
|
|
||||||
1, // 1: geometry
|
|
||||||
8, // 8 vertices
|
|
||||||
12, // 12 faces
|
|
||||||
|
|
||||||
// Face indices
|
|
||||||
0, 1, 2, 0, 2, 3,
|
|
||||||
0, 3, 4, 4, 5, 0,
|
|
||||||
6, 1, 0, 5, 6, 0,
|
|
||||||
7, 2, 1, 6, 7, 1,
|
|
||||||
2, 7, 3, 3, 7, 4,
|
|
||||||
7, 6, 4, 4, 6, 5,
|
|
||||||
|
|
||||||
// Bottom plate
|
|
||||||
p, n, n, p, n, p, n, n, p, n, n, n,
|
|
||||||
|
|
||||||
// Top plate
|
|
||||||
n, p, n, p, p, n, p, p, p, n, p, p,
|
|
||||||
|
|
||||||
// Normals
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
calculate_normals(&mut cube_geometry);
|
|
||||||
cube_geometry
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 Stem Node
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// nodes/max/plantarium/stem/src/lib.rs
|
|
||||||
|
|
||||||
nodarium_definition_file!("src/input.json");
|
|
||||||
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(
|
|
||||||
origin: *const i32,
|
|
||||||
amount: *const i32,
|
|
||||||
length: *const i32,
|
|
||||||
thickness: *const i32,
|
|
||||||
resolution: *const i32,
|
|
||||||
) -> Vec<i32> {
|
|
||||||
use nodarium_utils::{
|
|
||||||
read_vec3, read_i32, read_f32,
|
|
||||||
geometry::{create_multiple_paths, wrap_multiple_paths},
|
|
||||||
};
|
|
||||||
|
|
||||||
let origin = unsafe { read_vec3(origin) };
|
|
||||||
let amount = unsafe { read_i32(amount) } as usize;
|
|
||||||
let length = unsafe { read_f32(length) };
|
|
||||||
let thickness = unsafe { read_f32(thickness) };
|
|
||||||
let resolution = unsafe { read_i32(resolution) } as usize;
|
|
||||||
|
|
||||||
let mut stem_data = create_multiple_paths(amount, resolution, 1);
|
|
||||||
let mut stems = wrap_multiple_paths(&mut stem_data);
|
|
||||||
|
|
||||||
for stem in stems.iter_mut() {
|
|
||||||
let points = stem.get_points_mut();
|
|
||||||
for (i, point) in points.iter_mut().enumerate() {
|
|
||||||
let t = i as f32 / (resolution as f32 - 1.0);
|
|
||||||
point.x = origin[0];
|
|
||||||
point.y = origin[1] + t * length;
|
|
||||||
point.z = origin[2];
|
|
||||||
point.w = thickness * (1.0 - t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stem_data
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 5: Runtime Implementation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// app/src/lib/runtime/memory-manager.ts
|
|
||||||
|
|
||||||
export const SHARED_MEMORY = new WebAssembly.Memory({
|
|
||||||
initial: 1024, // 64MB initial
|
|
||||||
maximum: 4096, // 256MB maximum
|
|
||||||
});
|
|
||||||
|
|
||||||
export class MemoryManager {
|
|
||||||
private offset: number = 0;
|
|
||||||
private readonly start: number = 0;
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.offset = this.start;
|
|
||||||
}
|
|
||||||
|
|
||||||
alloc(bytes: number): number {
|
|
||||||
const pos = this.offset;
|
|
||||||
this.offset += bytes;
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
readInt32(pos: number): number {
|
|
||||||
return new Int32Array(SHARED_MEMORY.buffer)[pos / 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
readFloat32(pos: number): number {
|
|
||||||
return new Float32Array(SHARED_MEMORY.buffer)[pos / 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
readBytes(pos: number, length: number): Uint8Array {
|
|
||||||
return new Uint8Array(SHARED_MEMORY.buffer, pos, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInt32View(): Int32Array {
|
|
||||||
return new Int32Array(SHARED_MEMORY.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFloat32View(): Float32Array {
|
|
||||||
return new Float32Array(SHARED_MEMORY.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRemaining(): number {
|
|
||||||
return SHARED_MEMORY.buffer.byteLength - this.offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// app/src/lib/runtime/imports.ts
|
|
||||||
|
|
||||||
import { SHARED_MEMORY } from "./memory-manager";
|
|
||||||
|
|
||||||
export function createImportObject(nodeId: string): WebAssembly.Imports {
|
|
||||||
return {
|
|
||||||
env: {
|
|
||||||
// Import shared memory
|
|
||||||
memory: SHARED_MEMORY,
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
__nodarium_log: (ptr: number, len: number) => {
|
|
||||||
const msg = new TextDecoder().decode(
|
|
||||||
new Uint8Array(SHARED_MEMORY.buffer, ptr, len),
|
|
||||||
);
|
|
||||||
console.log(`[${nodeId}] ${msg}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
__nodarium_log_panic: (ptr: number, len: number) => {
|
|
||||||
const msg = new TextDecoder().decode(
|
|
||||||
new Uint8Array(SHARED_MEMORY.buffer, ptr, len),
|
|
||||||
);
|
|
||||||
console.error(`[${nodeId}] PANIC: ${msg}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// app/src/lib/runtime/executor.ts
|
|
||||||
|
|
||||||
import { SHARED_MEMORY } from "./memory-manager";
|
|
||||||
import { createImportObject } from "./imports";
|
|
||||||
|
|
||||||
export class SharedMemoryRuntimeExecutor implements RuntimeExecutor {
|
|
||||||
private memory: MemoryManager;
|
|
||||||
private results: Map<string, { pos: number; len: number }> = new Map();
|
|
||||||
private instances: Map<string, WebAssembly.Instance> = new Map();
|
|
||||||
|
|
||||||
constructor(private registry: NodeRegistry) {
|
|
||||||
this.memory = new MemoryManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
|
||||||
this.memory.reset();
|
|
||||||
this.results.clear();
|
|
||||||
|
|
||||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
|
||||||
const sortedNodes = nodes.sort((a, b) => b.depth - a.depth);
|
|
||||||
|
|
||||||
for (const node of sortedNodes) {
|
|
||||||
await this.executeNode(node, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = this.results.get(outputNode.id);
|
|
||||||
const view = this.memory.getInt32View();
|
|
||||||
return view.subarray(result.pos / 4, result.pos / 4 + result.len / 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeNode(
|
|
||||||
node: RuntimeNode,
|
|
||||||
settings: Record<string, unknown>,
|
|
||||||
) {
|
|
||||||
const def = this.definitionMap.get(node.type)!;
|
|
||||||
const inputs = def.inputs || {};
|
|
||||||
const inputNames = Object.keys(inputs);
|
|
||||||
|
|
||||||
const outputSize = this.estimateOutputSize(def);
|
|
||||||
const outputPos = this.memory.alloc(outputSize);
|
|
||||||
const args: number[] = [outputPos];
|
|
||||||
|
|
||||||
for (const inputName of inputNames) {
|
|
||||||
const inputDef = inputs[inputName];
|
|
||||||
const inputNode = node.state.inputNodes[inputName];
|
|
||||||
if (inputNode) {
|
|
||||||
const parentResult = this.results.get(inputNode.id)!;
|
|
||||||
args.push(parentResult.pos);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valuePos = this.memory.alloc(16);
|
|
||||||
this.writeValue(
|
|
||||||
valuePos,
|
|
||||||
inputDef,
|
|
||||||
node.props?.[inputName] ??
|
|
||||||
settings[inputDef.setting ?? ""] ??
|
|
||||||
inputDef.value,
|
|
||||||
);
|
|
||||||
args.push(valuePos);
|
|
||||||
}
|
|
||||||
|
|
||||||
let instance = this.instances.get(node.type);
|
|
||||||
if (!instance) {
|
|
||||||
instance = await this.instantiateNode(node.type);
|
|
||||||
this.instances.set(node.type, instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
const writtenLen = instance.exports.execute(...args);
|
|
||||||
this.results.set(node.id, { pos: outputPos, len: writtenLen });
|
|
||||||
}
|
|
||||||
|
|
||||||
private writeValue(pos: number, inputDef: NodeInput, value: unknown) {
|
|
||||||
const view = this.memory.getFloat32View();
|
|
||||||
const intView = this.memory.getInt32View();
|
|
||||||
|
|
||||||
switch (inputDef.type) {
|
|
||||||
case "float":
|
|
||||||
view[pos / 4] = value as number;
|
|
||||||
break;
|
|
||||||
case "integer":
|
|
||||||
case "select":
|
|
||||||
case "seed":
|
|
||||||
intView[pos / 4] = value as number;
|
|
||||||
break;
|
|
||||||
case "boolean":
|
|
||||||
intView[pos / 4] = value ? 1 : 0;
|
|
||||||
break;
|
|
||||||
case "vec3":
|
|
||||||
const arr = value as number[];
|
|
||||||
view[pos / 4] = arr[0];
|
|
||||||
view[pos / 4 + 1] = arr[1];
|
|
||||||
view[pos / 4 + 2] = arr[2];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private estimateOutputSize(def: NodeDefinition): number {
|
|
||||||
const sizes: Record<string, number> = {
|
|
||||||
float: 16,
|
|
||||||
integer: 16,
|
|
||||||
boolean: 16,
|
|
||||||
vec3: 16,
|
|
||||||
geometry: 8192,
|
|
||||||
path: 4096,
|
|
||||||
};
|
|
||||||
return sizes[def.outputs?.[0] || "float"] || 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async instantiateNode(
|
|
||||||
nodeType: string,
|
|
||||||
): Promise<WebAssembly.Instance> {
|
|
||||||
const wasmBytes = await this.fetchWasm(nodeType);
|
|
||||||
const module = await WebAssembly.compile(wasmBytes);
|
|
||||||
const importObject = createImportObject(nodeType);
|
|
||||||
return WebAssembly.instantiate(module, importObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 7: Execution Flow Visualization
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Execution Timeline │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
Step 1: Setup
|
|
||||||
SHARED_MEMORY = new WebAssembly.Memory({ initial: 1024 })
|
|
||||||
memory.offset = 0
|
|
||||||
|
|
||||||
Step 2: Execute Node A (math with 3 inputs)
|
|
||||||
outputPos = memory.alloc(16) = 0
|
|
||||||
args = [0, ptr_to_op_type, ptr_to_a, ptr_to_b]
|
|
||||||
|
|
||||||
Node A reads:
|
|
||||||
*ptr_to_op_type → op
|
|
||||||
*ptr_to_a → a
|
|
||||||
*ptr_to_b → b
|
|
||||||
|
|
||||||
Node A returns: vec![result.to_bits()]
|
|
||||||
|
|
||||||
Macro writes result directly to SHARED_MEMORY[0..4]
|
|
||||||
Returns: 4
|
|
||||||
|
|
||||||
results['A'] = { pos: 0, len: 4 }
|
|
||||||
memory.offset = 4
|
|
||||||
|
|
||||||
Step 3: Execute Node B (stem with 5 inputs, input[0] from A)
|
|
||||||
outputPos = memory.alloc(4096) = 4
|
|
||||||
args = [4, results['A'].pos, ptr_to_amount, ptr_to_length, ...]
|
|
||||||
|
|
||||||
Node B reads:
|
|
||||||
*results['A'].pos → value from Node A
|
|
||||||
*ptr_to_amount → amount
|
|
||||||
...
|
|
||||||
|
|
||||||
Node B returns: stem_data Vec<i32> (1000 elements = 4000 bytes)
|
|
||||||
|
|
||||||
Macro writes stem_data directly to SHARED_MEMORY[4..4004]
|
|
||||||
Returns: 4000
|
|
||||||
|
|
||||||
results['B'] = { pos: 4, len: 4000 }
|
|
||||||
memory.offset = 4004
|
|
||||||
|
|
||||||
Step 4: Execute Node C (output, 1 input from B)
|
|
||||||
outputPos = memory.alloc(16) = 4004
|
|
||||||
args = [4004, results['B'].pos, results['B'].len]
|
|
||||||
|
|
||||||
Node C reads:
|
|
||||||
*results['B'].pos → stem geometry
|
|
||||||
|
|
||||||
Node C returns: vec![1] (identity)
|
|
||||||
Macro writes to SHARED_MEMORY[4004..4008]
|
|
||||||
|
|
||||||
results['C'] = { pos: 4004, len: 4 }
|
|
||||||
|
|
||||||
Final: Return SHARED_MEMORY[4004..4008] as geometry result
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 6: Memory Growth Strategy
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class MemoryManager {
|
|
||||||
alloc(bytes: number): number {
|
|
||||||
const required = this.offset + bytes;
|
|
||||||
const currentBytes = SHARED_MEMORY.buffer.byteLength;
|
|
||||||
|
|
||||||
if (required > currentBytes) {
|
|
||||||
const pagesNeeded = Math.ceil((required - currentBytes) / 65536);
|
|
||||||
const success = SHARED_MEMORY.grow(pagesNeeded);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(`Out of memory: need ${bytes} bytes`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.int32View = new Int32Array(SHARED_MEMORY.buffer);
|
|
||||||
this.float32View = new Float32Array(SHARED_MEMORY.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = this.offset;
|
|
||||||
this.offset += bytes;
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 8: Migration Checklist
|
|
||||||
|
|
||||||
### Build Configuration
|
|
||||||
|
|
||||||
- [ ] Add `--import-memory` to Rust flags in `Cargo.toml`
|
|
||||||
- [ ] Ensure no nodes export memory
|
|
||||||
|
|
||||||
### Runtime
|
|
||||||
|
|
||||||
- [ ] Create `SHARED_MEMORY` instance
|
|
||||||
- [ ] Implement `MemoryManager` with alloc/read/write
|
|
||||||
- [ ] Create import object factory
|
|
||||||
- [ ] Implement `SharedMemoryRuntimeExecutor`
|
|
||||||
|
|
||||||
### Macro
|
|
||||||
|
|
||||||
- [ ] Parse definition JSON
|
|
||||||
- [ ] Validate function signature (N params, Vec<i32> return)
|
|
||||||
- [ ] Generate wrapper that writes return value to `output_pos`
|
|
||||||
- [ ] Add panic hook
|
|
||||||
|
|
||||||
### Utilities
|
|
||||||
|
|
||||||
- [ ] `read_i32(ptr: *const i32) -> i32`
|
|
||||||
- [ ] `read_f32(ptr: *const i32) -> f32`
|
|
||||||
- [ ] `read_bool(ptr: *const i32) -> bool`
|
|
||||||
- [ ] `read_vec3(ptr: *const i32) -> [f32; 3]`
|
|
||||||
- [ ] `read_i32_slice(ptr: *const i32, len: usize) -> &[i32]`
|
|
||||||
|
|
||||||
### Nodes
|
|
||||||
|
|
||||||
- [ ] `float`, `integer`, `boolean` nodes
|
|
||||||
- [ ] `vec3` node
|
|
||||||
- [ ] `math` node
|
|
||||||
- [ ] `random` node
|
|
||||||
- [ ] `box` node
|
|
||||||
- [ ] `stem` node
|
|
||||||
- [ ] `branch` node
|
|
||||||
- [ ] `instance` node
|
|
||||||
- [ ] `output` node
|
|
||||||
|
|
||||||
## Phase 9: Before vs After
|
|
||||||
|
|
||||||
### Before (per-node memory)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
|
||||||
let args = split_args(input);
|
|
||||||
let a = evaluate_float(args[0]);
|
|
||||||
let b = evaluate_float(args[1]);
|
|
||||||
vec![(a + b).to_bits()]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (shared memory)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(a: *const i32, b: *const i32) -> Vec<i32> {
|
|
||||||
use nodarium_utils::read_f32;
|
|
||||||
let a_val = unsafe { read_f32(a) };
|
|
||||||
let b_val = unsafe { read_f32(b) };
|
|
||||||
vec![(a_val + b_val).to_bits()]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key differences:**
|
|
||||||
|
|
||||||
- Parameters are input pointers, not a slice
|
|
||||||
- Use `read_f32` helper instead of `evaluate_float`
|
|
||||||
- Macro writes result directly to shared memory
|
|
||||||
- All nodes share the same memory import
|
|
||||||
|
|
||||||
## Phase 10: Benefits
|
|
||||||
|
|
||||||
| Aspect | Before | After |
|
|
||||||
| ----------------- | -------------- | -------------------- |
|
|
||||||
| Memory | N × ~1MB heaps | 1 × 64-256MB shared |
|
|
||||||
| Cross-node access | Copy via JS | Direct read |
|
|
||||||
| API | `&[i32]` slice | `*const i32` pointer |
|
|
||||||
| Validation | Runtime | Compile-time |
|
|
||||||
227
SUMMARY.md
227
SUMMARY.md
@@ -1,227 +0,0 @@
|
|||||||
# Nodarium - AI Coding Agent Summary
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Nodarium is a WebAssembly-based visual programming language used to build <https://nodes.max-richter.dev>, a procedural 3D plant modeling tool. The system allows users to create visual node graphs where each node is a compiled WebAssembly module.
|
|
||||||
|
|
||||||
## Technology Stack
|
|
||||||
|
|
||||||
**Frontend (SvelteKit):**
|
|
||||||
|
|
||||||
- Framework: SvelteKit with Svelte 5
|
|
||||||
- 3D Rendering: Three.js via Threlte
|
|
||||||
- Styling: Tailwind CSS 4
|
|
||||||
- Build Tool: Vite
|
|
||||||
- State Management: Custom store-client package
|
|
||||||
- WASM Integration: vite-plugin-wasm, comlink
|
|
||||||
|
|
||||||
**Backend/Core (Rust/WASM):**
|
|
||||||
|
|
||||||
- Language: Rust
|
|
||||||
- Output: WebAssembly (wasm32-unknown-unknown target)
|
|
||||||
- Build Tool: cargo
|
|
||||||
- Procedural Macros: custom macros package
|
|
||||||
|
|
||||||
**Package Management:**
|
|
||||||
|
|
||||||
- Node packages: pnpm workspace (v10.28.1)
|
|
||||||
- Rust packages: Cargo workspace
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
nodarium/
|
|
||||||
├── app/ # SvelteKit web application
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── lib/ # App-specific components and utilities
|
|
||||||
│ │ ├── routes/ # SvelteKit routes (pages)
|
|
||||||
│ │ ├── app.css # Global styles
|
|
||||||
│ │ └── app.html # HTML template
|
|
||||||
│ ├── static/
|
|
||||||
│ │ └── nodes/ # Compiled WASM node files served statically
|
|
||||||
│ ├── package.json # App dependencies
|
|
||||||
│ ├── svelte.config.js # SvelteKit configuration
|
|
||||||
│ ├── vite.config.ts # Vite configuration
|
|
||||||
│ └── tsconfig.json # TypeScript configuration
|
|
||||||
│
|
|
||||||
├── packages/ # Shared workspace packages
|
|
||||||
│ ├── ui/ # Svelte UI component library (published as @nodarium/ui)
|
|
||||||
│ │ ├── src/ # UI components
|
|
||||||
│ │ ├── static/ # Static assets for UI
|
|
||||||
│ │ ├── dist/ # Built output
|
|
||||||
│ │ └── package.json
|
|
||||||
│ ├── registry/ # Node registry with IndexedDB persistence (@nodarium/registry)
|
|
||||||
│ │ └── src/
|
|
||||||
│ ├── types/ # Shared TypeScript types (@nodarium/types)
|
|
||||||
│ │ └── src/
|
|
||||||
│ ├── utils/ # Shared utilities (@nodarium/utils)
|
|
||||||
│ │ └── src/
|
|
||||||
│ └── macros/ # Rust procedural macros for node development
|
|
||||||
│
|
|
||||||
├── nodes/ # WebAssembly node packages (Rust)
|
|
||||||
│ └── max/plantarium/ # Plantarium nodes namespace
|
|
||||||
│ ├── box/ # Box geometry node
|
|
||||||
│ ├── branch/ # Branch generation node
|
|
||||||
│ ├── float/ # Float value node
|
|
||||||
│ ├── gravity/ # Gravity simulation node
|
|
||||||
│ ├── instance/ # Geometry instancing node
|
|
||||||
│ ├── math/ # Math operations node
|
|
||||||
│ ├── noise/ # Noise generation node
|
|
||||||
│ ├── output/ # Output node for results
|
|
||||||
│ ├── random/ # Random value node
|
|
||||||
│ ├── rotate/ # Rotation transformation node
|
|
||||||
│ ├── stem/ # Stem geometry node
|
|
||||||
│ ├── triangle/ # Triangle geometry node
|
|
||||||
│ ├── vec3/ # Vector3 manipulation node
|
|
||||||
│ └── .template/ # Node template for creating new nodes
|
|
||||||
│
|
|
||||||
├── docs/ # Documentation
|
|
||||||
│ ├── ARCHITECTURE.md # System architecture overview
|
|
||||||
│ ├── DEVELOPING_NODES.md # Guide for creating new nodes
|
|
||||||
│ ├── NODE_DEFINITION.md # Node definition schema
|
|
||||||
│ └── PLANTARIUM.md # Plantarium-specific documentation
|
|
||||||
│
|
|
||||||
├── Cargo.toml # Rust workspace configuration
|
|
||||||
├── package.json # Root npm scripts
|
|
||||||
├── pnpm-workspace.yaml # pnpm workspace configuration
|
|
||||||
├── pnpm-lock.yaml # Locked dependency versions
|
|
||||||
└── README.md # Project readme
|
|
||||||
```
|
|
||||||
|
|
||||||
## Node System Architecture
|
|
||||||
|
|
||||||
### What is a Node?
|
|
||||||
|
|
||||||
Nodes are WebAssembly modules that:
|
|
||||||
|
|
||||||
- Have a unique ID (e.g., `max/plantarium/stem`)
|
|
||||||
- Define inputs with types and default values
|
|
||||||
- Define outputs they produce
|
|
||||||
- Execute logic when called with arguments
|
|
||||||
|
|
||||||
### Node Definition Schema
|
|
||||||
|
|
||||||
Nodes are defined via `definition.json` embedded in each WASM module:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "namespace/category/node-name",
|
|
||||||
"outputs": ["geometry"],
|
|
||||||
"inputs": {
|
|
||||||
"height": { "type": "float", "value": 1.0 },
|
|
||||||
"radius": { "type": "float", "value": 0.1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For now the outputs are limited to a single output.
|
|
||||||
|
|
||||||
### Node Execution
|
|
||||||
|
|
||||||
Nodes receive serialized arguments and return serialized outputs. The `nodarium_utils` Rust crate provides helpers for:
|
|
||||||
|
|
||||||
- Parsing input arguments
|
|
||||||
- Creating geometry data
|
|
||||||
- Concatenating output vectors
|
|
||||||
|
|
||||||
### Node Registration
|
|
||||||
|
|
||||||
Nodes are:
|
|
||||||
|
|
||||||
1. Compiled to WASM files in `target/wasm32-unknown-unknown/release/`
|
|
||||||
2. Copied to `app/static/nodes/` for serving
|
|
||||||
3. Registered in the browser via IndexedDB using the registry package
|
|
||||||
|
|
||||||
## Key Dependencies
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
|
|
||||||
- `@sveltejs/kit` - Application framework
|
|
||||||
- `@threlte/core` & `@threlte/extras` - Three.js Svelte integration
|
|
||||||
- `three` - 3D graphics library
|
|
||||||
- `tailwindcss` - CSS framework
|
|
||||||
- `comlink` - WebWorker RPC
|
|
||||||
- `idb` - IndexedDB wrapper
|
|
||||||
- `wabt` - WebAssembly binary toolkit
|
|
||||||
|
|
||||||
**Rust/WASM:**
|
|
||||||
|
|
||||||
- Language: Rust (compiled with plain cargo)
|
|
||||||
- Output: WebAssembly (wasm32-unknown-unknown target)
|
|
||||||
- Generic WASM wrapper for language-agnostic node development
|
|
||||||
- `glam` - Math library (Vec2, Vec3, Mat4, etc.)
|
|
||||||
- `nodarium_macros` - Custom procedural macros
|
|
||||||
- `nodarium_utils` - Shared node utilities
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
From root directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
pnpm i
|
|
||||||
|
|
||||||
# Build all WASM nodes (compiles Rust, copies to app/static)
|
|
||||||
pnpm build:nodes
|
|
||||||
|
|
||||||
# Build the app (builds UI library + SvelteKit app)
|
|
||||||
pnpm build:app
|
|
||||||
|
|
||||||
# Full build (nodes + app)
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# Development
|
|
||||||
pnpm dev # Run all dev commands in parallel
|
|
||||||
pnpm dev:nodes # Watch nodes/, auto-rebuild on changes
|
|
||||||
pnpm dev:app_ui # Watch app and UI package
|
|
||||||
pnpm dev_ui # Watch UI package only
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workspace Packages
|
|
||||||
|
|
||||||
The project uses pnpm workspaces with the following packages:
|
|
||||||
|
|
||||||
| Package | Location | Purpose |
|
|
||||||
| ------------------ | ------------------ | ------------------------------ |
|
|
||||||
| @nodarium/app | app/ | Main SvelteKit application |
|
|
||||||
| @nodarium/ui | packages/ui/ | Reusable UI component library |
|
|
||||||
| @nodarium/registry | packages/registry/ | Node registry with persistence |
|
|
||||||
| @nodarium/types | packages/types/ | Shared TypeScript types |
|
|
||||||
| @nodarium/utils | packages/utils/ | Shared utilities |
|
|
||||||
| nodarium macros | packages/macros/ | Rust procedural macros |
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
- `.dprint.jsonc` - Dprint formatter configuration
|
|
||||||
- `svelte.config.js` - SvelteKit configuration (app and ui)
|
|
||||||
- `vite.config.ts` - Vite bundler configuration
|
|
||||||
- `tsconfig.json` - TypeScript configuration (app and packages)
|
|
||||||
- `Cargo.toml` - Rust workspace with member packages
|
|
||||||
- `flake.nix` - Nix development environment
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Adding a New Node
|
|
||||||
|
|
||||||
1. Copy the `.template` directory in `nodes/max/plantarium/` to create a new node directory
|
|
||||||
2. Define node in `src/definition.json`
|
|
||||||
3. Implement logic in `src/lib.rs`
|
|
||||||
4. Build with `cargo build --release --target wasm32-unknown-unknown`
|
|
||||||
5. Test by dragging onto the node graph
|
|
||||||
|
|
||||||
### Modifying UI Components
|
|
||||||
|
|
||||||
1. Changes to `packages/ui/` automatically rebuild with watch mode
|
|
||||||
2. App imports from `@nodarium/ui`
|
|
||||||
3. Run `pnpm dev:app_ui` for hot reload
|
|
||||||
|
|
||||||
## Important Notes for AI Agents
|
|
||||||
|
|
||||||
1. **WASM Compilation**: Nodes require `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`)
|
|
||||||
2. **Cross-Compilation**: WASM build happens on host, not in containers/VMs
|
|
||||||
3. **Static Serving**: Compiled WASM files must exist in `app/static/nodes/` before dev server runs
|
|
||||||
4. **Workspace Dependencies**: Use `workspace:*` protocol for internal packages
|
|
||||||
5. **Threlte Version**: Uses Threlte 8.x, not 7.x (important for 3D component APIs)
|
|
||||||
6. **Svelte 5**: Project uses Svelte 5 with runes (`$state`, `$derived`, `$effect`)
|
|
||||||
7. **Tailwind 4**: Uses Tailwind CSS v4 with `@tailwindcss/vite` plugin
|
|
||||||
8. **IndexedDB**: Registry uses IDB for persistent node storage in browser
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
# Node Compilation and Runtime Execution
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Nodarium nodes are WebAssembly modules written in Rust. Each node is a compiled WASM binary that exposes a standardized C ABI interface. The system uses procedural macros to generate the necessary boilerplate for node definitions, memory management, and execution.
|
|
||||||
|
|
||||||
## Node Compilation
|
|
||||||
|
|
||||||
### 1. Node Definition (JSON)
|
|
||||||
|
|
||||||
Each node has a `src/input.json` file that defines:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "max/plantarium/stem",
|
|
||||||
"meta": { "description": "Creates a stem" },
|
|
||||||
"outputs": ["path"],
|
|
||||||
"inputs": {
|
|
||||||
"origin": { "type": "vec3", "value": [0, 0, 0], "external": true },
|
|
||||||
"amount": { "type": "integer", "value": 1, "min": 1, "max": 64 },
|
|
||||||
"length": { "type": "float", "value": 5 },
|
|
||||||
"thickness": { "type": "float", "value": 0.2 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Procedural Macros
|
|
||||||
|
|
||||||
The `nodarium_macros` crate provides two procedural macros:
|
|
||||||
|
|
||||||
#### `#[nodarium_execute]`
|
|
||||||
|
|
||||||
Transforms a Rust function into a WASM-compatible entry point:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[nodarium_execute]
|
|
||||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
|
||||||
// Node logic here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The macro generates:
|
|
||||||
- **C ABI wrapper**: Converts the WASM interface to a standard C FFI
|
|
||||||
- **`execute` function**: Takes `(ptr: *const i32, len: usize)` and returns `*mut i32`
|
|
||||||
- **Memory allocation**: `__alloc(len: usize) -> *mut i32` for buffer allocation
|
|
||||||
- **Memory deallocation**: `__free(ptr: *mut i32, len: usize)` for cleanup
|
|
||||||
- **Static output buffer**: `OUTPUT_BUFFER` for returning results
|
|
||||||
- **Panic hook**: Routes panics through `host_log_panic` for debugging
|
|
||||||
- **Internal logic wrapper**: Wraps the original function
|
|
||||||
|
|
||||||
#### `nodarium_definition_file!("path")`
|
|
||||||
|
|
||||||
Embeds the node definition JSON into the WASM binary:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
nodarium_definition_file!("src/input.json");
|
|
||||||
```
|
|
||||||
|
|
||||||
Generates:
|
|
||||||
- **`DEFINITION_DATA`**: Static byte array in `nodarium_definition` section
|
|
||||||
- **`get_definition_ptr()`**: Returns pointer to definition data
|
|
||||||
- **`get_definition_len()`**: Returns length of definition data
|
|
||||||
|
|
||||||
### 3. Build Process
|
|
||||||
|
|
||||||
Nodes are compiled with:
|
|
||||||
```bash
|
|
||||||
cargo build --release --target wasm32-unknown-unknown
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting `.wasm` files are copied to `app/static/nodes/` for serving.
|
|
||||||
|
|
||||||
## Node Execution Runtime
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ WebWorker Thread │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────┐│
|
|
||||||
│ │ WorkerRuntimeExecutor ││
|
|
||||||
│ │ ┌───────────────────────────────────────────────────┐ ││
|
|
||||||
│ │ │ MemoryRuntimeExecutor ││
|
|
||||||
│ │ │ ┌─────────────────────────────────────────────┐ ││
|
|
||||||
│ │ │ │ Node Registry (WASM + Definitions) ││
|
|
||||||
│ │ │ └─────────────────────────────────────────────┘ ││
|
|
||||||
│ │ │ ┌─────────────────────────────────────────────┐ ││
|
|
||||||
│ │ │ │ Execution Engine (Bottom-Up Evaluation) ││
|
|
||||||
│ │ │ └─────────────────────────────────────────────┘ ││
|
|
||||||
│ │ └───────────────────────────────────────────────────┘ ││
|
|
||||||
│ └─────────────────────────────────────────────────────────┘│
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1. MemoryRuntimeExecutor
|
|
||||||
|
|
||||||
The core execution engine in `runtime-executor.ts`:
|
|
||||||
|
|
||||||
#### Metadata Collection (`addMetaData`)
|
|
||||||
|
|
||||||
1. Load node definitions from registry
|
|
||||||
2. Build parent/child relationships from graph edges
|
|
||||||
3. Calculate execution depth via reverse BFS from output node
|
|
||||||
|
|
||||||
#### Node Sorting
|
|
||||||
|
|
||||||
Nodes are sorted by depth (highest depth first) for bottom-up execution:
|
|
||||||
|
|
||||||
```
|
|
||||||
Depth 3: n3 n6
|
|
||||||
Depth 2: n2 n4 n5
|
|
||||||
Depth 1: n1
|
|
||||||
Depth 0: Output
|
|
||||||
Execution order: n3, n6, n2, n4, n5, n1, Output
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Input Collection
|
|
||||||
|
|
||||||
For each node, inputs are gathered from:
|
|
||||||
1. **Connected nodes**: Results from parent nodes in the graph
|
|
||||||
2. **Node props**: Values stored directly on the node instance
|
|
||||||
3. **Settings**: Global settings mapped via `setting` property
|
|
||||||
4. **Defaults**: Values from node definition
|
|
||||||
|
|
||||||
#### Input Encoding
|
|
||||||
|
|
||||||
Values are encoded as `Int32Array`:
|
|
||||||
- **Floats**: IEEE 754 bits cast to i32
|
|
||||||
- **Vectors**: `[0, count, v1, v2, v3, 1, 1]` (nested bracket format)
|
|
||||||
- **Booleans**: `0` or `1`
|
|
||||||
- **Integers**: Direct i32 value
|
|
||||||
|
|
||||||
#### Caching
|
|
||||||
|
|
||||||
Results are cached using:
|
|
||||||
```typescript
|
|
||||||
inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`
|
|
||||||
```
|
|
||||||
|
|
||||||
The cache uses LRU eviction (default size: 50 entries).
|
|
||||||
|
|
||||||
### 2. Execution Flow
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async execute(graph: Graph, settings) {
|
|
||||||
// 1. Load definitions and build node relationships
|
|
||||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
|
||||||
|
|
||||||
// 2. Sort nodes by depth (bottom-up)
|
|
||||||
const sortedNodes = nodes.sort((a, b) => b.depth - a.depth);
|
|
||||||
|
|
||||||
// 3. Execute each node
|
|
||||||
for (const node of sortedNodes) {
|
|
||||||
const inputs = this.collectInputs(node, settings);
|
|
||||||
const encoded = concatEncodedArrays(inputs);
|
|
||||||
const result = nodeType.execute(encoded);
|
|
||||||
this.results[node.id] = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Return output node result
|
|
||||||
return this.results[outputNode.id];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Worker Isolation
|
|
||||||
|
|
||||||
`WorkerRuntimeExecutor` runs execution in a WebWorker via Comlink:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class WorkerRuntimeExecutor implements RuntimeExecutor {
|
|
||||||
private worker = new ComlinkWorker(...);
|
|
||||||
|
|
||||||
async execute(graph, settings) {
|
|
||||||
return this.worker.executeGraph(graph, settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The worker backend (`worker-runtime-executor-backend.ts`):
|
|
||||||
- Creates a single `MemoryRuntimeExecutor` instance
|
|
||||||
- Manages caching state
|
|
||||||
- Collects performance metrics
|
|
||||||
|
|
||||||
### 4. Remote Execution (Optional)
|
|
||||||
|
|
||||||
`RemoteRuntimeExecutor` can execute graphs on a remote server:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class RemoteRuntimeExecutor implements RuntimeExecutor {
|
|
||||||
async execute(graph, settings) {
|
|
||||||
const res = await fetch(this.url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ graph, settings })
|
|
||||||
});
|
|
||||||
return new Int32Array(await res.arrayBuffer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Encoding Format
|
|
||||||
|
|
||||||
### Bracket Notation
|
|
||||||
|
|
||||||
Inputs and outputs use a nested bracket encoding:
|
|
||||||
|
|
||||||
```
|
|
||||||
[0, count, item1, item2, ..., 1, 1]
|
|
||||||
^ ^ items ^ ^
|
|
||||||
| | | |
|
|
||||||
| | | +-- closing bracket
|
|
||||||
| +-- number of items + 1 |
|
|
||||||
+-- opening bracket (0) +-- closing bracket (1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Encodings
|
|
||||||
|
|
||||||
**Float (5.0)**:
|
|
||||||
```typescript
|
|
||||||
encodeFloat(5.0) // → 1084227584 (IEEE 754 bits as i32)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vec3 ([1, 2, 3])**:
|
|
||||||
```typescript
|
|
||||||
[0, 4, encodeFloat(1), encodeFloat(2), encodeFloat(3), 1, 1]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nested Math Expression**:
|
|
||||||
```
|
|
||||||
[0, 3, 0, 2, 0, 3, 0, 0, 0, 3, 7549747, 127, 1, 1, ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Decoding Utilities
|
|
||||||
|
|
||||||
From `packages/utils/src/tree.rs`:
|
|
||||||
- `split_args()`: Parses nested bracket arrays into segments
|
|
||||||
- `evaluate_float()`: Recursively evaluates and decodes float expressions
|
|
||||||
- `evaluate_int()`: Evaluates integer/math node expressions
|
|
||||||
- `evaluate_vec3()`: Decodes vec3 arrays
|
|
||||||
|
|
||||||
## Geometry Data Format
|
|
||||||
|
|
||||||
### Path Data
|
|
||||||
|
|
||||||
Paths represent procedural plant structures:
|
|
||||||
|
|
||||||
```
|
|
||||||
[0, count, [0, header_size, node_type, depth, x, y, z, w, ...], 1, 1]
|
|
||||||
```
|
|
||||||
|
|
||||||
Each point has 4 values: x, y, z position + thickness (w).
|
|
||||||
|
|
||||||
### Geometry Data
|
|
||||||
|
|
||||||
Meshes use a similar format with vertices and face indices.
|
|
||||||
|
|
||||||
## Performance Tracking
|
|
||||||
|
|
||||||
The runtime collects detailed performance metrics:
|
|
||||||
- `collect-metadata`: Time to build node graph
|
|
||||||
- `collected-inputs`: Time to gather inputs
|
|
||||||
- `encoded-inputs`: Time to encode inputs
|
|
||||||
- `hash-inputs`: Time to compute cache hash
|
|
||||||
- `cache-hit`: 1 if cache hit, 0 if miss
|
|
||||||
- `node/{node_type}`: Time per node execution
|
|
||||||
|
|
||||||
## Caching Strategy
|
|
||||||
|
|
||||||
### MemoryRuntimeCache
|
|
||||||
|
|
||||||
LRU cache implementation:
|
|
||||||
```typescript
|
|
||||||
class MemoryRuntimeCache {
|
|
||||||
private map = new Map<string, unknown>();
|
|
||||||
size: number = 50;
|
|
||||||
|
|
||||||
get(key) { /* move to front */ }
|
|
||||||
set(key, value) { /* evict oldest if at capacity */ }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IndexDBCache
|
|
||||||
|
|
||||||
For persistence across sessions, the registry uses IndexedDB caching.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The Nodarium node system works as follows:
|
|
||||||
|
|
||||||
1. **Compilation**: Rust functions are decorated with macros that generate C ABI WASM exports
|
|
||||||
2. **Registration**: Node definitions are embedded in WASM and loaded at runtime
|
|
||||||
3. **Graph Analysis**: Runtime builds node relationships and execution order
|
|
||||||
4. **Bottom-Up Execution**: Nodes execute from leaves to output
|
|
||||||
5. **Caching**: Results are cached per-node-inputs hash for performance
|
|
||||||
6. **Isolation**: Execution runs in a WebWorker to prevent main thread blocking
|
|
||||||
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@@ -27,3 +27,5 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
test-results/
|
||||||
|
|||||||
@@ -25,20 +25,8 @@ FROM nginx:alpine AS runner
|
|||||||
|
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
COPY <<EOF /etc/nginx/conf.d/app.conf
|
COPY app/docker/app.conf /etc/nginx/conf.d/app.conf
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
root /app;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
COPY --from=builder /app/app/build /app
|
COPY --from=builder /app/app/build /app
|
||||||
COPY --from=builder /app/packages/ui/build /app/ui
|
|
||||||
|
|
||||||
EXPOSE 80
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/e2e/main.test.ts
Normal file
62
app/e2e/main.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
// Listen for console messages
|
||||||
|
page.on('console', msg => {
|
||||||
|
console.log(`[Browser Console] ${msg.type()}: ${msg.text()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('http://localhost:4173', { waitUntil: 'load' });
|
||||||
|
|
||||||
|
// await expect(page).toHaveScreenshot();
|
||||||
|
await expect(page.locator('.graph-wrapper')).toHaveScreenshot();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'projects' }).click();
|
||||||
|
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||||
|
await page.getByRole('combobox').selectOption('2');
|
||||||
|
await page.getByRole('textbox', { name: 'Project name' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Project name' }).fill('Test Project');
|
||||||
|
await page.getByRole('button', { name: 'Create' }).click();
|
||||||
|
|
||||||
|
const expectedNodes = [
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
type: 'max/plantarium/stem',
|
||||||
|
props: {
|
||||||
|
amount: 50,
|
||||||
|
length: 4,
|
||||||
|
thickness: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '11',
|
||||||
|
type: 'max/plantarium/noise',
|
||||||
|
props: {
|
||||||
|
scale: 0.5,
|
||||||
|
strength: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
type: 'max/plantarium/output'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const node of expectedNodes) {
|
||||||
|
const wrapper = page.locator(
|
||||||
|
`div.wrapper[data-node-id="${node.id}"][data-node-type="${node.type}"]`
|
||||||
|
);
|
||||||
|
await expect(wrapper).toBeVisible();
|
||||||
|
if ('props' in node) {
|
||||||
|
const props = node.props as unknown as Record<string, number>;
|
||||||
|
for (const propId in node.props) {
|
||||||
|
const expectedValue = props[propId];
|
||||||
|
const inputElement = page.locator(
|
||||||
|
`div.wrapper[data-node-type="${node.type}"][data-node-input="${propId}"] input[type="number"]`
|
||||||
|
);
|
||||||
|
const value = parseFloat(await inputElement.inputValue());
|
||||||
|
expect(value).toBe(expectedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
app/e2e/main.test.ts-snapshots/test-1-linux.png
Normal file
BIN
app/e2e/main.test.ts-snapshots/test-1-linux.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "@nodarium/app",
|
"name": "@nodarium/app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "svelte-kit sync && vite build",
|
"build": "svelte-kit sync && vite build",
|
||||||
"test": "vitest",
|
"test:unit": "vitest",
|
||||||
"preview": "vite preview"
|
"test": "npm run test:unit -- --run && npm run test:e2e",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"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": {
|
"dependencies": {
|
||||||
"@nodarium/registry": "workspace:*",
|
|
||||||
"@nodarium/ui": "workspace:*",
|
"@nodarium/ui": "workspace:*",
|
||||||
"@nodarium/utils": "workspace:*",
|
"@nodarium/utils": "workspace:*",
|
||||||
"@sveltejs/kit": "^2.50.0",
|
"@sveltejs/kit": "^2.50.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@threlte/core": "8.3.1",
|
"@threlte/core": "8.3.1",
|
||||||
"@threlte/extras": "9.7.1",
|
"@threlte/extras": "9.7.1",
|
||||||
@@ -21,27 +26,37 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"jsondiffpatch": "^0.7.3",
|
"jsondiffpatch": "^0.7.3",
|
||||||
|
"micromark": "^4.0.2",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"three": "^0.182.0",
|
"three": "^0.182.0"
|
||||||
"wabt": "^1.0.39"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^2.0.2",
|
||||||
|
"@eslint/js": "^9.39.2",
|
||||||
"@iconify-json/tabler": "^1.2.26",
|
"@iconify-json/tabler": "^1.2.26",
|
||||||
"@iconify/tailwind4": "^1.2.1",
|
"@iconify/tailwind4": "^1.2.1",
|
||||||
"@nodarium/types": "workspace:",
|
"@nodarium/types": "workspace:^",
|
||||||
|
"@playwright/test": "^1.58.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tsconfig/svelte": "^5.0.6",
|
"@tsconfig/svelte": "^5.0.7",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/three": "^0.182.0",
|
"@types/three": "^0.182.0",
|
||||||
"svelte": "^5.46.4",
|
"@vitest/browser-playwright": "^4.0.18",
|
||||||
"svelte-check": "^4.3.5",
|
"dprint": "^0.51.1",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-plugin-svelte": "^3.14.0",
|
||||||
|
"globals": "^17.3.0",
|
||||||
|
"svelte": "^5.49.2",
|
||||||
|
"svelte-check": "^4.3.6",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.54.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-comlink": "^5.3.0",
|
"vite-plugin-comlink": "^5.3.0",
|
||||||
"vite-plugin-glsl": "^1.5.5",
|
"vite-plugin-glsl": "^1.5.5",
|
||||||
"vite-plugin-wasm": "^3.5.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"vitest": "^4.0.17"
|
"vitest": "^4.0.18",
|
||||||
|
"vitest-browser-svelte": "^2.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
app/playwright.config.ts
Normal file
20
app/playwright.config.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
webServer: { command: 'pnpm build && pnpm preview', port: 4173 },
|
||||||
|
testDir: 'e2e',
|
||||||
|
use: {
|
||||||
|
browserName: 'firefox',
|
||||||
|
launchOptions: {
|
||||||
|
firefoxUserPrefs: {
|
||||||
|
// Force WebGL even without a GPU
|
||||||
|
'webgl.force-enabled': true,
|
||||||
|
'webgl.disabled': false,
|
||||||
|
// Use software rendering (Mesa) instead of hardware
|
||||||
|
'layers.acceleration.disabled': true,
|
||||||
|
'gfx.webrender.software': true,
|
||||||
|
'webgl.enable-webgl2': true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -2,5 +2,9 @@
|
|||||||
@source "../../packages/ui/**/*.svelte";
|
@source "../../packages/ui/**/*.svelte";
|
||||||
@plugin "@iconify/tailwind4" {
|
@plugin "@iconify/tailwind4" {
|
||||||
prefix: "i";
|
prefix: "i";
|
||||||
icon-sets: from-folder(custom, "./src/lib/icons")
|
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
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
// interface Locals {}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>
|
<body data-sveltekit-preload-data="hover">
|
||||||
<meta charset="utf-8" />
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
|
</body>
|
||||||
<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>
|
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
import { PUBLIC_ANALYTIC_SCRIPT } from "$env/static/public";
|
import { PUBLIC_ANALYTIC_SCRIPT } from '$env/static/public';
|
||||||
export const ANALYTIC_SCRIPT = PUBLIC_ANALYTIC_SCRIPT;
|
export const ANALYTIC_SCRIPT = PUBLIC_ANALYTIC_SCRIPT;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { T } from "@threlte/core";
|
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||||
import BackgroundVert from "./Background.vert";
|
import { T } from '@threlte/core';
|
||||||
import BackgroundFrag from "./Background.frag";
|
import { colors } from '../graph/colors.svelte';
|
||||||
import { colors } from "../graph/colors.svelte";
|
import BackgroundFrag from './Background.frag';
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import BackgroundVert from './Background.vert';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
minZoom: number;
|
minZoom: number;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
maxZoom = 150,
|
maxZoom = 150,
|
||||||
cameraPosition = [0, 1, 0],
|
cameraPosition = [0, 1, 0],
|
||||||
width = globalThis?.innerWidth || 100,
|
width = globalThis?.innerWidth || 100,
|
||||||
height = globalThis?.innerHeight || 100,
|
height = globalThis?.innerHeight || 100
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let bw = $derived(width / cameraPosition[2]);
|
let bw = $derived(width / cameraPosition[2]);
|
||||||
@@ -38,25 +38,25 @@
|
|||||||
fragmentShader={BackgroundFrag}
|
fragmentShader={BackgroundFrag}
|
||||||
uniforms={{
|
uniforms={{
|
||||||
camPos: {
|
camPos: {
|
||||||
value: [0, 1, 0],
|
value: [0, 1, 0]
|
||||||
},
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
value: colors["layer-0"],
|
value: colors['layer-0']
|
||||||
},
|
},
|
||||||
lineColor: {
|
lineColor: {
|
||||||
value: colors["outline"],
|
value: colors['outline']
|
||||||
},
|
},
|
||||||
zoomLimits: {
|
zoomLimits: {
|
||||||
value: [2, 50],
|
value: [2, 50]
|
||||||
},
|
},
|
||||||
dimensions: {
|
dimensions: {
|
||||||
value: [100, 100],
|
value: [100, 100]
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
uniforms.camPos.value={cameraPosition}
|
uniforms.camPos.value={cameraPosition}
|
||||||
uniforms.backgroundColor.value={appSettings.value.theme &&
|
uniforms.backgroundColor.value={appSettings.value.theme
|
||||||
colors["layer-0"]}
|
&& colors['layer-0']}
|
||||||
uniforms.lineColor.value={appSettings.value.theme && colors["outline"]}
|
uniforms.lineColor.value={appSettings.value.theme && colors['outline']}
|
||||||
uniforms.zoomLimits.value={[minZoom, maxZoom]}
|
uniforms.zoomLimits.value={[minZoom, maxZoom]}
|
||||||
uniforms.dimensions.value={[width, height]}
|
uniforms.dimensions.value={[width, height]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#each nodes as node}
|
{#each nodes as node (node.id)}
|
||||||
<div
|
<div
|
||||||
class="result"
|
class="result"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background: var(--layer-0);
|
background: var(--color-layer-0);
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -168,10 +168,10 @@
|
|||||||
|
|
||||||
.add-menu-wrapper {
|
.add-menu-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--layer-1);
|
background: var(--color-layer-1);
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: solid 2px var(--layer-2);
|
border: solid 2px var(--color-layer-2);
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
@@ -184,14 +184,14 @@
|
|||||||
|
|
||||||
.result {
|
.result {
|
||||||
padding: 1em 0.9em;
|
padding: 1em 0.9em;
|
||||||
border-bottom: solid 1px var(--layer-2);
|
border-bottom: solid 1px var(--color-layer-2);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result[aria-selected="true"] {
|
.result[aria-selected="true"] {
|
||||||
background: var(--layer-2);
|
background: var(--color-layer-2);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { HTML } from "@threlte/extras";
|
import { HTML } from '@threlte/extras';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
p1: { x: number; y: number };
|
p1: { x: number; y: number };
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
const {
|
const {
|
||||||
p1 = { x: 0, y: 0 },
|
p1 = { x: 0, y: 0 },
|
||||||
p2 = { x: 0, y: 0 },
|
p2 = { x: 0, y: 0 },
|
||||||
cameraPosition = [0, 1, 0],
|
cameraPosition = [0, 1, 0]
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const width = $derived(Math.abs(p1.x - p2.x) * cameraPosition[2]);
|
const width = $derived(Math.abs(p1.x - p2.x) * cameraPosition[2]);
|
||||||
@@ -24,14 +24,15 @@
|
|||||||
<div
|
<div
|
||||||
class="box-selection"
|
class="box-selection"
|
||||||
style={`width: ${width}px; height: ${height}px;`}
|
style={`width: ${width}px; height: ${height}px;`}
|
||||||
></div>
|
>
|
||||||
|
</div>
|
||||||
</HTML>
|
</HTML>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.box-selection {
|
.box-selection {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: solid 2px var(--outline);
|
border: solid 2px var(--color-outline);
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { T } from "@threlte/core";
|
import { T } from '@threlte/core';
|
||||||
import { type OrthographicCamera } from "three";
|
import { type OrthographicCamera } from 'three';
|
||||||
type Props = {
|
type Props = {
|
||||||
camera: OrthographicCamera;
|
camera: OrthographicCamera;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeDefinition, NodeRegistry } from "@nodarium/types";
|
import type { NodeDefinition, NodeRegistry } from '@nodarium/types';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let mx = $state(0);
|
let mx = $state(0);
|
||||||
let my = $state(0);
|
let my = $state(0);
|
||||||
@@ -20,15 +20,15 @@
|
|||||||
my = ev.clientY;
|
my = ev.clientY;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const closest = target?.closest?.("[data-node-type]");
|
const closest = target?.closest?.('[data-node-type]');
|
||||||
|
|
||||||
if (!closest) {
|
if (!closest) {
|
||||||
node = undefined;
|
node = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodeType = closest.getAttribute("data-node-type");
|
let nodeType = closest.getAttribute('data-node-type');
|
||||||
let nodeInput = closest.getAttribute("data-node-input");
|
let nodeInput = closest.getAttribute('data-node-input');
|
||||||
|
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
node = undefined;
|
node = undefined;
|
||||||
@@ -40,9 +40,9 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const style = wrapper.parentElement?.style;
|
const style = wrapper.parentElement?.style;
|
||||||
style?.setProperty("cursor", "help");
|
style?.setProperty('cursor', 'help');
|
||||||
return () => {
|
return () => {
|
||||||
style?.removeProperty("cursor");
|
style?.removeProperty('cursor');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -53,12 +53,12 @@
|
|||||||
class="help-wrapper p-4"
|
class="help-wrapper p-4"
|
||||||
class:visible={node}
|
class:visible={node}
|
||||||
bind:clientWidth={width}
|
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}
|
bind:this={wrapper}
|
||||||
>
|
>
|
||||||
<p class="m-0 text-light opacity-40 flex items-center gap-3 mb-4">
|
<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>
|
<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}
|
{#if input}
|
||||||
<span>> {input}</span>
|
<span>> {input}</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{#if !input}
|
{#if !input}
|
||||||
<div>
|
<div>
|
||||||
<span class="i-tabler-arrow-right opacity-30">-></span>
|
<span class="i-tabler-arrow-right opacity-30">-></span>
|
||||||
{node?.outputs?.map((o) => o).join(", ") ?? "nothing"}
|
{node?.outputs?.map((o) => o).join(', ') ?? 'nothing'}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -88,12 +88,12 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translate(var(--mx), var(--my));
|
transform: translate(var(--mx), var(--my));
|
||||||
background: var(--layer-1);
|
background: var(--color-layer-1);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
border: 1px solid var(--outline);
|
border: 1px solid var(--color-outline);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
import type { Box } from '@nodarium/types';
|
||||||
import { points, lines, rects } from "./store.js";
|
import { T } from '@threlte/core';
|
||||||
import { T } from "@threlte/core";
|
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||||
import { Color } from "three";
|
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>
|
</script>
|
||||||
|
|
||||||
{#each $points as point}
|
{#each $points as point (getEachKey(point))}
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
position.x={point.x}
|
position.x={point.x}
|
||||||
position.y={point.y}
|
position.y={point.y}
|
||||||
@@ -17,7 +38,7 @@
|
|||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each $rects as rect, i}
|
{#each $rects as rect, i (getEachKey(rect))}
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
position.x={(rect.minX + rect.maxX) / 2}
|
position.x={(rect.minX + rect.maxX) / 2}
|
||||||
position.y={0}
|
position.y={0}
|
||||||
@@ -32,11 +53,11 @@
|
|||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each $lines as line}
|
{#each $lines as line (getEachKey(line))}
|
||||||
<T.Mesh position.y={1}>
|
<T.Mesh position.y={1}>
|
||||||
<MeshLineGeometry points={line.points} />
|
<MeshLineGeometry points={line.points} />
|
||||||
<MeshLineMaterial
|
<MeshLineMaterial
|
||||||
color={line.color || "red"}
|
color={line.color || 'red'}
|
||||||
linewidth={1}
|
linewidth={1}
|
||||||
attenuate={false}
|
attenuate={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { colors } from "../graph/colors.svelte";
|
import { colors } from '../graph/colors.svelte';
|
||||||
|
|
||||||
const circleMaterial = new MeshBasicMaterial({
|
const circleMaterial = new MeshBasicMaterial({
|
||||||
color: colors.edge.clone(),
|
color: colors.edge.clone(),
|
||||||
toneMapped: false,
|
toneMapped: false
|
||||||
});
|
});
|
||||||
|
|
||||||
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
|
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
appSettings.value.theme;
|
if (appSettings.value.theme === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
|
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
|
||||||
lineColor = colors.edge.clone().convertSRGBToLinear();
|
lineColor = colors.edge.clone().convertSRGBToLinear();
|
||||||
});
|
});
|
||||||
@@ -20,19 +22,19 @@
|
|||||||
new Vector2(0, 0),
|
new Vector2(0, 0),
|
||||||
new Vector2(0, 0),
|
new Vector2(0, 0),
|
||||||
new Vector2(0, 0),
|
new Vector2(0, 0),
|
||||||
new Vector2(0, 0),
|
new Vector2(0, 0)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { T } from "@threlte/core";
|
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
import { T } from '@threlte/core';
|
||||||
import { MeshBasicMaterial, Vector3 } from "three";
|
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||||
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
|
import { onDestroy } from 'svelte';
|
||||||
import { Vector2 } from "three/src/math/Vector2.js";
|
import { MeshBasicMaterial, Vector3 } from 'three';
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import { CubicBezierCurve } from 'three/src/extras/curves/CubicBezierCurve.js';
|
||||||
import { getGraphState } from "../graph-state.svelte";
|
import { Vector2 } from 'three/src/math/Vector2.js';
|
||||||
import { onDestroy } from "svelte";
|
import { getGraphState } from '../graph-state.svelte';
|
||||||
|
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
lastId = curveId;
|
lastId = curveId;
|
||||||
|
|
||||||
const length = Math.floor(
|
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);
|
const samples = Math.max(length * 16, 10);
|
||||||
@@ -83,7 +85,7 @@
|
|||||||
id,
|
id,
|
||||||
x1,
|
x1,
|
||||||
y1,
|
y1,
|
||||||
$state.snapshot(points) as unknown as Vector3[],
|
$state.snapshot(points) as unknown as Vector3[]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
export const setXYZXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
|
export const setXYZXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
|
||||||
array[location + 0] = x
|
array[location + 0] = x;
|
||||||
array[location + 1] = y
|
array[location + 1] = y;
|
||||||
array[location + 2] = z
|
array[location + 2] = z;
|
||||||
|
|
||||||
array[location + 3] = x
|
array[location + 3] = x;
|
||||||
array[location + 4] = y
|
array[location + 4] = y;
|
||||||
array[location + 5] = z
|
array[location + 5] = z;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const setXY = (array: number[], location: number, x: number, y: number) => {
|
export const setXY = (array: number[], location: number, x: number, y: number) => {
|
||||||
array[location + 0] = x
|
array[location + 0] = x;
|
||||||
array[location + 1] = y
|
array[location + 1] = y;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const setXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
|
export const setXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
|
||||||
array[location + 0] = x
|
array[location + 0] = x;
|
||||||
array[location + 1] = y
|
array[location + 1] = y;
|
||||||
array[location + 2] = z
|
array[location + 2] = z;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const setXYZW = (
|
export const setXYZW = (
|
||||||
array: number[],
|
array: number[],
|
||||||
@@ -27,8 +27,8 @@ export const setXYZW = (
|
|||||||
z: number,
|
z: number,
|
||||||
w: number
|
w: number
|
||||||
) => {
|
) => {
|
||||||
array[location + 0] = x
|
array[location + 0] = x;
|
||||||
array[location + 1] = y
|
array[location + 1] = y;
|
||||||
array[location + 2] = z
|
array[location + 2] = z;
|
||||||
array[location + 3] = w
|
array[location + 3] = w;
|
||||||
}
|
};
|
||||||
|
|||||||
265
app/src/lib/graph-interface/graph-manager.svelte.test.ts
Normal file
265
app/src/lib/graph-interface/graph-manager.svelte.test.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { GraphManager } from './graph-manager.svelte';
|
||||||
|
import {
|
||||||
|
createMockNodeRegistry,
|
||||||
|
mockFloatInputNode,
|
||||||
|
mockFloatOutputNode,
|
||||||
|
mockGeometryOutputNode,
|
||||||
|
mockPathInputNode,
|
||||||
|
mockVec3OutputNode
|
||||||
|
} from './test-utils';
|
||||||
|
|
||||||
|
describe('GraphManager', () => {
|
||||||
|
describe('getPossibleSockets', () => {
|
||||||
|
describe('when dragging an output socket', () => {
|
||||||
|
it('should return compatible input sockets based on type', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockFloatOutputNode,
|
||||||
|
mockFloatInputNode,
|
||||||
|
mockGeometryOutputNode,
|
||||||
|
mockPathInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const floatInputNode = manager.createNode({
|
||||||
|
type: 'test/node/input',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const floatOutputNode = manager.createNode({
|
||||||
|
type: 'test/node/output',
|
||||||
|
position: [0, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(floatInputNode).toBeDefined();
|
||||||
|
expect(floatOutputNode).toBeDefined();
|
||||||
|
|
||||||
|
const possibleSockets = manager.getPossibleSockets({
|
||||||
|
node: floatOutputNode!,
|
||||||
|
index: 0,
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(possibleSockets.length).toBe(1);
|
||||||
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
||||||
|
expect(socketNodeIds).toContain(floatInputNode!.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude self node from possible sockets', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockFloatOutputNode,
|
||||||
|
mockFloatInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const floatInputNode = manager.createNode({
|
||||||
|
type: 'test/node/input',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(floatInputNode).toBeDefined();
|
||||||
|
|
||||||
|
const possibleSockets = manager.getPossibleSockets({
|
||||||
|
node: floatInputNode!,
|
||||||
|
index: 'value',
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
||||||
|
expect(socketNodeIds).not.toContain(floatInputNode!.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude parent nodes from possible sockets when dragging output', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockFloatOutputNode,
|
||||||
|
mockFloatInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const parentNode = manager.createNode({
|
||||||
|
type: 'test/node/output',
|
||||||
|
position: [0, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const childNode = manager.createNode({
|
||||||
|
type: 'test/node/input',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parentNode).toBeDefined();
|
||||||
|
expect(childNode).toBeDefined();
|
||||||
|
|
||||||
|
if (parentNode && childNode) {
|
||||||
|
manager.createEdge(parentNode, 0, childNode, 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
const possibleSockets = manager.getPossibleSockets({
|
||||||
|
node: parentNode!,
|
||||||
|
index: 0,
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
||||||
|
expect(socketNodeIds).not.toContain(childNode!.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sockets compatible with accepts property', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockGeometryOutputNode,
|
||||||
|
mockPathInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const geometryOutputNode = manager.createNode({
|
||||||
|
type: 'test/node/geometry',
|
||||||
|
position: [0, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathInputNode = manager.createNode({
|
||||||
|
type: 'test/node/path',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(geometryOutputNode).toBeDefined();
|
||||||
|
expect(pathInputNode).toBeDefined();
|
||||||
|
|
||||||
|
const possibleSockets = manager.getPossibleSockets({
|
||||||
|
node: geometryOutputNode!,
|
||||||
|
index: 0,
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
||||||
|
expect(socketNodeIds).toContain(pathInputNode!.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when no compatible sockets exist', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockVec3OutputNode,
|
||||||
|
mockFloatInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const vec3OutputNode = manager.createNode({
|
||||||
|
type: 'test/node/vec3',
|
||||||
|
position: [0, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const floatInputNode = manager.createNode({
|
||||||
|
type: 'test/node/input',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(vec3OutputNode).toBeDefined();
|
||||||
|
expect(floatInputNode).toBeDefined();
|
||||||
|
|
||||||
|
const possibleSockets = manager.getPossibleSockets({
|
||||||
|
node: vec3OutputNode!,
|
||||||
|
index: 0,
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
||||||
|
expect(socketNodeIds).not.toContain(floatInputNode!.id);
|
||||||
|
expect(possibleSockets.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return socket info with correct socket key for inputs', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockFloatOutputNode,
|
||||||
|
mockFloatInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const floatOutputNode = manager.createNode({
|
||||||
|
type: 'test/node/output',
|
||||||
|
position: [0, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const floatInputNode = manager.createNode({
|
||||||
|
type: 'test/node/input',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(floatOutputNode).toBeDefined();
|
||||||
|
expect(floatInputNode).toBeDefined();
|
||||||
|
|
||||||
|
const possibleSockets = manager.getPossibleSockets({
|
||||||
|
node: floatOutputNode!,
|
||||||
|
index: 0,
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchingSocket = possibleSockets.find(([node]) => node.id === floatInputNode!.id);
|
||||||
|
expect(matchingSocket).toBeDefined();
|
||||||
|
expect(matchingSocket![1]).toBe('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return multiple compatible sockets', () => {
|
||||||
|
const registry = createMockNodeRegistry([
|
||||||
|
mockFloatOutputNode,
|
||||||
|
mockFloatInputNode,
|
||||||
|
mockGeometryOutputNode,
|
||||||
|
mockPathInputNode
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
const floatOutputNode = manager.createNode({
|
||||||
|
type: 'test/node/output',
|
||||||
|
position: [0, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const geometryOutputNode = manager.createNode({
|
||||||
|
type: 'test/node/geometry',
|
||||||
|
position: [200, 0],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const floatInputNode = manager.createNode({
|
||||||
|
type: 'test/node/input',
|
||||||
|
position: [100, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathInputNode = manager.createNode({
|
||||||
|
type: 'test/node/path',
|
||||||
|
position: [300, 100],
|
||||||
|
props: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(floatOutputNode).toBeDefined();
|
||||||
|
expect(geometryOutputNode).toBeDefined();
|
||||||
|
expect(floatInputNode).toBeDefined();
|
||||||
|
expect(pathInputNode).toBeDefined();
|
||||||
|
|
||||||
|
const possibleSocketsForFloat = manager.getPossibleSockets({
|
||||||
|
node: floatOutputNode!,
|
||||||
|
index: 0,
|
||||||
|
position: [0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(possibleSocketsForFloat.length).toBe(1);
|
||||||
|
expect(possibleSocketsForFloat.map(([n]) => n.id)).toContain(floatInputNode!.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import throttle from '$lib/helpers/throttle';
|
import throttle from '$lib/helpers/throttle';
|
||||||
import { RemoteNodeRegistry } from '@nodarium/registry';
|
import { RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||||
import type {
|
import type {
|
||||||
Edge,
|
Edge,
|
||||||
Graph,
|
Graph,
|
||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
} from '@nodarium/types';
|
} from '@nodarium/types';
|
||||||
import { fastHashString } from '@nodarium/utils';
|
import { fastHashString } from '@nodarium/utils';
|
||||||
import { createLogger } from '@nodarium/utils';
|
import { createLogger } from '@nodarium/utils';
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
import EventEmitter from './helpers/EventEmitter';
|
import EventEmitter from './helpers/EventEmitter';
|
||||||
import { HistoryManager } from './history-manager';
|
import { HistoryManager } from './history-manager';
|
||||||
|
|
||||||
@@ -23,16 +23,16 @@ const remoteRegistry = new RemoteNodeRegistry('');
|
|||||||
|
|
||||||
const clone = 'structuredClone' in self
|
const clone = 'structuredClone' in self
|
||||||
? self.structuredClone
|
? self.structuredClone
|
||||||
: (args: any) => JSON.parse(JSON.stringify(args));
|
: (args: unknown) => JSON.parse(JSON.stringify(args));
|
||||||
|
|
||||||
export function areSocketsCompatible(
|
function areSocketsCompatible(
|
||||||
output: string | undefined,
|
output: string | undefined,
|
||||||
inputs: string | (string | undefined)[] | undefined
|
inputs: string | (string | undefined)[] | undefined
|
||||||
) {
|
) {
|
||||||
if (Array.isArray(inputs) && output) {
|
if (Array.isArray(inputs) && output) {
|
||||||
return inputs.includes('*') || inputs.includes(output);
|
return inputs.includes(output);
|
||||||
}
|
}
|
||||||
return inputs === output || inputs === '*';
|
return inputs === output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
||||||
@@ -57,7 +57,7 @@ function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
|||||||
|
|
||||||
export class GraphManager extends EventEmitter<{
|
export class GraphManager extends EventEmitter<{
|
||||||
save: Graph;
|
save: Graph;
|
||||||
result: any;
|
result: unknown;
|
||||||
settings: {
|
settings: {
|
||||||
types: Record<string, NodeInput>;
|
types: Record<string, NodeInput>;
|
||||||
values: Record<string, unknown>;
|
values: Record<string, unknown>;
|
||||||
@@ -79,7 +79,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
currentUndoGroup: number | null = null;
|
currentUndoGroup: number | null = null;
|
||||||
|
|
||||||
inputSockets = $derived.by(() => {
|
inputSockets = $derived.by(() => {
|
||||||
const s = new Set<string>();
|
const s = new SvelteSet<string>();
|
||||||
for (const edge of this.edges) {
|
for (const edge of this.edges) {
|
||||||
s.add(`${edge[2].id}-${edge[3]}`);
|
s.add(`${edge[2].id}-${edge[3]}`);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
private lastSettingsHash = 0;
|
private lastSettingsHash = 0;
|
||||||
setSettings(settings: Record<string, unknown>) {
|
setSettings(settings: Record<string, unknown>) {
|
||||||
let hash = fastHashString(JSON.stringify(settings));
|
const hash = fastHashString(JSON.stringify(settings));
|
||||||
if (hash === this.lastSettingsHash) return;
|
if (hash === this.lastSettingsHash) return;
|
||||||
this.lastSettingsHash = hash;
|
this.lastSettingsHash = hash;
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLinkedNodes(node: NodeInstance) {
|
getLinkedNodes(node: NodeInstance) {
|
||||||
const nodes = new Set<NodeInstance>();
|
const nodes = new SvelteSet<NodeInstance>();
|
||||||
const stack = [node];
|
const stack = [node];
|
||||||
while (stack.length) {
|
while (stack.length) {
|
||||||
const n = stack.pop();
|
const n = stack.pop();
|
||||||
@@ -171,7 +171,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
||||||
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
||||||
|
|
||||||
const bestInputEntry = draggedInputs.find(([_, input]) => {
|
const bestInputEntry = draggedInputs.find(([, input]) => {
|
||||||
const accepted = [input.type, ...(input.accepts || [])];
|
const accepted = [input.type, ...(input.accepts || [])];
|
||||||
return areSocketsCompatible(edgeOutputSocketType, accepted);
|
return areSocketsCompatible(edgeOutputSocketType, accepted);
|
||||||
});
|
});
|
||||||
@@ -209,7 +209,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
||||||
|
|
||||||
// Optimization: Pre-calculate parents to avoid cycles
|
// Optimization: Pre-calculate parents to avoid cycles
|
||||||
const parentIds = new Set(this.getParentsOfNode(draggedNode).map(n => n.id));
|
const parentIds = new SvelteSet(this.getParentsOfNode(draggedNode).map(n => n.id));
|
||||||
|
|
||||||
return this.edges.filter((edge) => {
|
return this.edges.filter((edge) => {
|
||||||
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
||||||
@@ -266,9 +266,16 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _init(graph: Graph) {
|
private _init(graph: Graph) {
|
||||||
const nodes = new Map(
|
const nodes = new SvelteMap(
|
||||||
graph.nodes.map((node) => {
|
graph.nodes.map((node) => {
|
||||||
return [node.id, node as NodeInstance];
|
const nodeType = this.registry.getNode(node.type);
|
||||||
|
const n = node as NodeInstance;
|
||||||
|
if (nodeType) {
|
||||||
|
n.state = {
|
||||||
|
type: nodeType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return [node.id, n];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -293,30 +300,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.execute();
|
this.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadAllCollections() {
|
|
||||||
// Fetch all nodes from all collections of the loaded nodes
|
|
||||||
const nodeIds = Array.from(new Set([...this.graph.nodes.map((n) => n.type)]));
|
|
||||||
const allCollections = new Set<`${string}/${string}`>();
|
|
||||||
for (const id of nodeIds) {
|
|
||||||
const [user, collection] = id.split('/');
|
|
||||||
allCollections.add(`${user}/${collection}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allCollectionIds = await Promise
|
|
||||||
.all([...allCollections]
|
|
||||||
.map(async (collection) =>
|
|
||||||
remoteRegistry
|
|
||||||
.fetchCollection(collection)
|
|
||||||
.then((collection: { nodes: { id: NodeId }[] }) => {
|
|
||||||
return collection.nodes.map(n => n.id.replace(/\.wasm$/, '') as NodeId);
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
const missingNodeIds = [...new Set(allCollectionIds.flat())];
|
|
||||||
|
|
||||||
this.registry.load(missingNodeIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
async load(graph: Graph) {
|
async load(graph: Graph) {
|
||||||
const a = performance.now();
|
const a = performance.now();
|
||||||
|
|
||||||
@@ -325,17 +308,26 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.status = 'loading';
|
this.status = 'loading';
|
||||||
this.id = graph.id;
|
this.id = graph.id;
|
||||||
|
|
||||||
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
|
logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id });
|
||||||
|
|
||||||
logger.info('loading graph', {
|
|
||||||
nodes: graph.nodes,
|
|
||||||
edges: graph.edges,
|
|
||||||
id: graph.id,
|
|
||||||
ids: nodeIds
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const nodeIds = Array.from(new SvelteSet([...graph.nodes.map((n) => n.type)]));
|
||||||
await this.registry.load(nodeIds);
|
await this.registry.load(nodeIds);
|
||||||
|
|
||||||
|
// 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());
|
logger.info('loaded node types', this.registry.getAllNodes());
|
||||||
|
|
||||||
for (const node of this.graph.nodes) {
|
for (const node of this.graph.nodes) {
|
||||||
@@ -362,7 +354,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
if (type.inputs) {
|
if (type.inputs) {
|
||||||
for (const key in type.inputs) {
|
for (const key in type.inputs) {
|
||||||
let settingId = type.inputs[key].setting;
|
const settingId = type.inputs[key].setting;
|
||||||
if (settingId) {
|
if (settingId) {
|
||||||
settingTypes[settingId] = {
|
settingTypes[settingId] = {
|
||||||
__node_type: type.id,
|
__node_type: type.id,
|
||||||
@@ -392,9 +384,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||||
|
|
||||||
setTimeout(() => this.execute(), 100);
|
setTimeout(() => this.execute(), 100);
|
||||||
this.loadAllCollections(); // lazily load all nodes from all collections
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllNodes() {
|
getAllNodes() {
|
||||||
@@ -419,7 +409,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const settingValues = this.settings;
|
const settingValues = this.settings;
|
||||||
if (nodeType.inputs) {
|
if (nodeType.inputs) {
|
||||||
for (const key in nodeType.inputs) {
|
for (const key in nodeType.inputs) {
|
||||||
let settingId = nodeType.inputs[key].setting;
|
const settingId = nodeType.inputs[key].setting;
|
||||||
if (settingId) {
|
if (settingId) {
|
||||||
settingTypes[settingId] = nodeType.inputs[key];
|
settingTypes[settingId] = nodeType.inputs[key];
|
||||||
if (
|
if (
|
||||||
@@ -501,10 +491,10 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const inputs = Object.entries(to.state?.type?.inputs ?? {});
|
const inputs = Object.entries(to.state?.type?.inputs ?? {});
|
||||||
const outputs = from.state?.type?.outputs ?? [];
|
const outputs = from.state?.type?.outputs ?? [];
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
const [inputName, input] = inputs[i];
|
const [inputName, input] = inputs[0];
|
||||||
for (let o = 0; o < outputs.length; o++) {
|
for (let o = 0; o < outputs.length; o++) {
|
||||||
const output = outputs[o];
|
const output = outputs[0];
|
||||||
if (input.type === output || input.type === '*') {
|
if (input.type === output) {
|
||||||
return this.createEdge(from, o, to, inputName);
|
return this.createEdge(from, o, to, inputName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,7 +507,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
|
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
|
||||||
// map old ids to new ids
|
// 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();
|
||||||
|
|
||||||
@@ -606,14 +596,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromType = from.state.type || this.registry.getNode(from.type);
|
|
||||||
const toType = to.state.type || this.registry.getNode(to.type);
|
|
||||||
|
|
||||||
// check if socket types match
|
// check if socket types match
|
||||||
const fromSocketType = fromType?.outputs?.[fromSocket];
|
const fromSocketType = from.state?.type?.outputs?.[fromSocket];
|
||||||
const toSocketType = [toType?.inputs?.[toSocket]?.type];
|
const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
|
||||||
if (toType?.inputs?.[toSocket]?.accepts) {
|
if (to.state?.type?.inputs?.[toSocket]?.accepts) {
|
||||||
toSocketType.push(...(toType?.inputs?.[toSocket]?.accepts || []));
|
toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||||
@@ -736,16 +723,15 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
||||||
const nodeType = this.registry.getNode(node.type);
|
const nodeType = node?.state?.type;
|
||||||
if (!nodeType) return [];
|
if (!nodeType) return [];
|
||||||
console.log({ index });
|
|
||||||
|
|
||||||
const sockets: [NodeInstance, string | number][] = [];
|
const sockets: [NodeInstance, string | number][] = [];
|
||||||
|
|
||||||
// if index is a string, we are an input looking for outputs
|
// 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
|
// 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(
|
const nodes = this.getAllNodes().filter(
|
||||||
(n) => n.id !== node.id && !children.has(n.id)
|
(n) => n.id !== node.id && !children.has(n.id)
|
||||||
);
|
);
|
||||||
@@ -753,7 +739,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const ownType = nodeType?.inputs?.[index].type;
|
const ownType = nodeType?.inputs?.[index].type;
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const nodeType = this.registry.getNode(node.type);
|
const nodeType = node?.state?.type;
|
||||||
const inputs = nodeType?.outputs;
|
const inputs = nodeType?.outputs;
|
||||||
if (!inputs) continue;
|
if (!inputs) continue;
|
||||||
for (let index = 0; index < inputs.length; index++) {
|
for (let index = 0; index < inputs.length; index++) {
|
||||||
@@ -766,22 +752,26 @@ export class GraphManager extends EventEmitter<{
|
|||||||
// if index is a number, we are an output looking for inputs
|
// if index is a number, we are an output looking for inputs
|
||||||
|
|
||||||
// filter out self and parent nodes
|
// 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(
|
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 SvelteMap<number, string[]>();
|
||||||
const edges = new Map(
|
this.getEdgesFromNode(node)
|
||||||
this.getEdgesFromNode(node)
|
.filter((e) => e[1] === index)
|
||||||
.filter((e) => e[1] === index)
|
.forEach((e) => {
|
||||||
.map((e) => [e[2].id, e[3]])
|
if (edges.has(e[2].id)) {
|
||||||
);
|
edges.get(e[2].id)?.push(e[3]);
|
||||||
|
} else {
|
||||||
|
edges.set(e[2].id, [e[3]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const ownType = nodeType.outputs?.[index];
|
const ownType = nodeType.outputs?.[index];
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const inputs = this.registry.getNode(node.type)?.inputs;
|
const inputs = node?.state?.type?.inputs;
|
||||||
if (!inputs) continue;
|
if (!inputs) continue;
|
||||||
for (const key in inputs) {
|
for (const key in inputs) {
|
||||||
const otherType = [inputs[key].type];
|
const otherType = [inputs[key].type];
|
||||||
@@ -789,7 +779,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
areSocketsCompatible(ownType, otherType)
|
areSocketsCompatible(ownType, otherType)
|
||||||
&& edges.get(node.id) !== key
|
&& !edges.get(node.id)?.includes(key)
|
||||||
) {
|
) {
|
||||||
sockets.push([node, key]);
|
sockets.push([node, key]);
|
||||||
}
|
}
|
||||||
@@ -797,7 +787,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${sockets.length} possible sockets`, sockets);
|
|
||||||
return sockets;
|
return sockets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NodeInstance, Socket } from '@nodarium/types';
|
import type { NodeInstance, Socket } from '@nodarium/types';
|
||||||
import { getContext, setContext } from 'svelte';
|
import { getContext, setContext } from 'svelte';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
import type { OrthographicCamera, Vector3 } from 'three';
|
import type { OrthographicCamera, Vector3 } from 'three';
|
||||||
import type { GraphManager } from './graph-manager.svelte';
|
import type { GraphManager } from './graph-manager.svelte';
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export class GraphState {
|
|||||||
height = $state(100);
|
height = $state(100);
|
||||||
|
|
||||||
hoveredEdgeId = $state<string | null>(null);
|
hoveredEdgeId = $state<string | null>(null);
|
||||||
edges = new Map<string, EdgeData>();
|
edges = new SvelteMap<string, EdgeData>();
|
||||||
|
|
||||||
wrapper = $state<HTMLDivElement>(null!);
|
wrapper = $state<HTMLDivElement>(null!);
|
||||||
rect: DOMRect = $derived(
|
rect: DOMRect = $derived(
|
||||||
@@ -100,7 +100,7 @@ export class GraphState {
|
|||||||
hoveredSocket = $state<Socket | null>(null);
|
hoveredSocket = $state<Socket | null>(null);
|
||||||
possibleSockets = $state<Socket[]>([]);
|
possibleSockets = $state<Socket[]>([]);
|
||||||
possibleSocketIds = $derived(
|
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() {
|
getEdges() {
|
||||||
@@ -155,7 +155,6 @@ export class GraphState {
|
|||||||
return 4;
|
return 4;
|
||||||
} else if (z > 11) {
|
} else if (z > 11) {
|
||||||
return 2;
|
return 2;
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -170,14 +169,11 @@ export class GraphState {
|
|||||||
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
|
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const inputs = node.state.type?.inputs || this.graph.registry.getNode(node.type)?.inputs
|
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
||||||
|| {};
|
return [
|
||||||
const _index = Object.keys(inputs).indexOf(index);
|
|
||||||
const pos = [
|
|
||||||
node?.state?.x ?? node.position[0],
|
node?.state?.x ?? node.position[0],
|
||||||
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
|
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
|
||||||
] as [number, number];
|
];
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +192,7 @@ export class GraphState {
|
|||||||
(p) =>
|
(p) =>
|
||||||
p !== 'seed'
|
p !== 'seed'
|
||||||
&& node?.inputs
|
&& node?.inputs
|
||||||
&& !('setting' in node?.inputs?.[p])
|
&& !(node?.inputs?.[p] !== undefined && 'setting' in node.inputs[p])
|
||||||
&& node.inputs[p].hidden !== true
|
&& node.inputs[p].hidden !== true
|
||||||
).length;
|
).length;
|
||||||
this.nodeHeightCache[nodeTypeId] = height;
|
this.nodeHeightCache[nodeTypeId] = height;
|
||||||
@@ -253,7 +249,7 @@ export class GraphState {
|
|||||||
|
|
||||||
let { node, index, position } = socket;
|
let { node, index, position } = socket;
|
||||||
|
|
||||||
// if the socket is an input socket -> remove existing edges
|
// remove existing edge
|
||||||
if (typeof index === 'string') {
|
if (typeof index === 'string') {
|
||||||
const edges = this.graph.getEdgesToNode(node);
|
const edges = this.graph.getEdgesToNode(node);
|
||||||
for (const edge of edges) {
|
for (const edge of edges) {
|
||||||
@@ -297,8 +293,8 @@ export class GraphState {
|
|||||||
getNodeIdFromEvent(event: MouseEvent) {
|
getNodeIdFromEvent(event: MouseEvent) {
|
||||||
let clickedNodeId = -1;
|
let clickedNodeId = -1;
|
||||||
|
|
||||||
let mx = event.clientX - this.rect.x;
|
const mx = event.clientX - this.rect.x;
|
||||||
let my = event.clientY - this.rect.y;
|
const my = event.clientY - this.rect.y;
|
||||||
|
|
||||||
if (event.button === 0) {
|
if (event.button === 0) {
|
||||||
// check if the clicked element is a node
|
// check if the clicked element is a node
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Edge, NodeInstance } from "@nodarium/types";
|
import type { Edge, NodeInstance } from '@nodarium/types';
|
||||||
import { Canvas } from "@threlte/core";
|
import { Canvas } from '@threlte/core';
|
||||||
import { HTML } from "@threlte/extras";
|
import { HTML } from '@threlte/extras';
|
||||||
import { createKeyMap } from "../../helpers/createKeyMap";
|
import { createKeyMap } from '../../helpers/createKeyMap';
|
||||||
import Background from "../background/Background.svelte";
|
import Background from '../background/Background.svelte';
|
||||||
import AddMenu from "../components/AddMenu.svelte";
|
import AddMenu from '../components/AddMenu.svelte';
|
||||||
import BoxSelection from "../components/BoxSelection.svelte";
|
import BoxSelection from '../components/BoxSelection.svelte';
|
||||||
import Camera from "../components/Camera.svelte";
|
import Camera from '../components/Camera.svelte';
|
||||||
import HelpView from "../components/HelpView.svelte";
|
import HelpView from '../components/HelpView.svelte';
|
||||||
import Debug from "../debug/Debug.svelte";
|
import Debug from '../debug/Debug.svelte';
|
||||||
import EdgeEl from "../edges/Edge.svelte";
|
import EdgeEl from '../edges/Edge.svelte';
|
||||||
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||||
import NodeEl from "../node/Node.svelte";
|
import NodeEl from '../node/Node.svelte';
|
||||||
import { maxZoom, minZoom } from "./constants";
|
import { maxZoom, minZoom } from './constants';
|
||||||
import { FileDropEventManager } from "./drop.events";
|
import { FileDropEventManager } from './drop.events';
|
||||||
import { MouseEventManager } from "./mouse.events";
|
import { MouseEventManager } from './mouse.events';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
keymap,
|
keymap
|
||||||
}: {
|
}: {
|
||||||
keymap: ReturnType<typeof createKeyMap>;
|
keymap: ReturnType<typeof createKeyMap>;
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -45,19 +45,18 @@
|
|||||||
const newNode = graph.createNode({
|
const newNode = graph.createNode({
|
||||||
type: node.type,
|
type: node.type,
|
||||||
position: node.position,
|
position: node.position,
|
||||||
props: node.props,
|
props: node.props
|
||||||
});
|
});
|
||||||
if (!newNode) return;
|
if (!newNode) return;
|
||||||
|
|
||||||
if (graphState.activeSocket) {
|
if (graphState.activeSocket) {
|
||||||
if (typeof graphState.activeSocket.index === "number") {
|
if (typeof graphState.activeSocket.index === 'number') {
|
||||||
const socketType =
|
const socketType = graphState.activeSocket.node.state?.type?.outputs?.[
|
||||||
graphState.activeSocket.node.state?.type?.outputs?.[
|
graphState.activeSocket.index
|
||||||
graphState.activeSocket.index
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
|
const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
|
||||||
(inp) => inp[1].type === socketType || inp[1].type === "*",
|
(inp) => inp[1].type === socketType
|
||||||
);
|
);
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
@@ -65,18 +64,17 @@
|
|||||||
graphState.activeSocket.node,
|
graphState.activeSocket.node,
|
||||||
graphState.activeSocket.index,
|
graphState.activeSocket.index,
|
||||||
newNode,
|
newNode,
|
||||||
input[0],
|
input[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const socketType =
|
const socketType = graphState.activeSocket.node.state?.type?.inputs?.[
|
||||||
graphState.activeSocket.node.state?.type?.inputs?.[
|
graphState.activeSocket.index
|
||||||
graphState.activeSocket.index
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const output = newNode.state?.type?.outputs?.find((out) => {
|
const output = newNode.state?.type?.outputs?.find((out) => {
|
||||||
if (socketType?.type === out) return true;
|
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;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +83,7 @@
|
|||||||
newNode,
|
newNode,
|
||||||
output.indexOf(output),
|
output.indexOf(output),
|
||||||
graphState.activeSocket.node,
|
graphState.activeSocket.node,
|
||||||
graphState.activeSocket.index,
|
graphState.activeSocket.index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,20 +146,18 @@
|
|||||||
<BoxSelection
|
<BoxSelection
|
||||||
cameraPosition={graphState.cameraPosition}
|
cameraPosition={graphState.cameraPosition}
|
||||||
p1={{
|
p1={{
|
||||||
x:
|
x: graphState.cameraPosition[0]
|
||||||
graphState.cameraPosition[0] +
|
+ (graphState.mouseDown[0] - graphState.width / 2)
|
||||||
(graphState.mouseDown[0] - graphState.width / 2) /
|
/ graphState.cameraPosition[2],
|
||||||
graphState.cameraPosition[2],
|
y: graphState.cameraPosition[1]
|
||||||
y:
|
+ (graphState.mouseDown[1] - graphState.height / 2)
|
||||||
graphState.cameraPosition[1] +
|
/ graphState.cameraPosition[2]
|
||||||
(graphState.mouseDown[1] - graphState.height / 2) /
|
|
||||||
graphState.cameraPosition[2],
|
|
||||||
}}
|
}}
|
||||||
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
|
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if graph.status === "idle"}
|
{#if graph.status === 'idle'}
|
||||||
{#if graphState.addMenuPosition}
|
{#if graphState.addMenuPosition}
|
||||||
<AddMenu onnode={handleNodeCreation} />
|
<AddMenu onnode={handleNodeCreation} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -176,7 +172,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each graph.edges as edge}
|
{#each graph.edges as edge (edge)}
|
||||||
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
||||||
<EdgeEl
|
<EdgeEl
|
||||||
id={graph.getEdgeId(edge)}
|
id={graph.getEdgeId(edge)}
|
||||||
@@ -208,9 +204,9 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</HTML>
|
</HTML>
|
||||||
{:else if graph.status === "loading"}
|
{:else if graph.status === 'loading'}
|
||||||
<span>Loading</span>
|
<span>Loading</span>
|
||||||
{:else if graph.status === "error"}
|
{:else if graph.status === 'error'}
|
||||||
<span>Error</span>
|
<span>Error</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
@@ -248,7 +244,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--layer-2);
|
background: var(--color-layer-2);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
input:disabled {
|
input:disabled {
|
||||||
@@ -268,8 +264,8 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
height: calc(100% - 25px);
|
height: calc(100% - 25px);
|
||||||
border: dashed 4px var(--layer-2);
|
border: dashed 4px var(--color-layer-2);
|
||||||
background: var(--layer-1);
|
background: var(--color-layer-1);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,46 +1,43 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Graph, NodeInstance, NodeRegistry } from "@nodarium/types";
|
import { createKeyMap } from '$lib/helpers/createKeyMap';
|
||||||
import GraphEl from "./Graph.svelte";
|
import type { Graph, NodeInstance, NodeRegistry } from '@nodarium/types';
|
||||||
import { GraphManager } from "../graph-manager.svelte";
|
import { GraphManager } from '../graph-manager.svelte';
|
||||||
import { createKeyMap } from "$lib/helpers/createKeyMap";
|
import { GraphState, setGraphManager, setGraphState } from '../graph-state.svelte';
|
||||||
import {
|
import { setupKeymaps } from '../keymaps';
|
||||||
GraphState,
|
import GraphEl from './Graph.svelte';
|
||||||
setGraphManager,
|
|
||||||
setGraphState,
|
|
||||||
} from "../graph-state.svelte";
|
|
||||||
import { setupKeymaps } from "../keymaps";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
graph?: Graph;
|
graph?: Graph;
|
||||||
registry: NodeRegistry;
|
registry: NodeRegistry;
|
||||||
|
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, unknown>;
|
||||||
|
|
||||||
activeNode?: NodeInstance;
|
activeNode?: NodeInstance;
|
||||||
showGrid?: boolean;
|
showGrid?: boolean;
|
||||||
snapToGrid?: boolean;
|
snapToGrid?: boolean;
|
||||||
showHelp?: boolean;
|
showHelp?: boolean;
|
||||||
settingTypes?: Record<string, any>;
|
settingTypes?: Record<string, unknown>;
|
||||||
|
|
||||||
onsave?: (save: Graph) => void;
|
onsave?: (save: Graph) => void;
|
||||||
onresult?: (result: any) => void;
|
onresult?: (result: unknown) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
graph,
|
graph,
|
||||||
registry,
|
registry,
|
||||||
|
settings = $bindable(),
|
||||||
activeNode = $bindable(),
|
activeNode = $bindable(),
|
||||||
showGrid = $bindable(true),
|
showGrid = $bindable(true),
|
||||||
snapToGrid = $bindable(true),
|
snapToGrid = $bindable(true),
|
||||||
showHelp = $bindable(false),
|
showHelp = $bindable(false),
|
||||||
settings = $bindable(),
|
|
||||||
settingTypes = $bindable(),
|
settingTypes = $bindable(),
|
||||||
onsave,
|
onsave,
|
||||||
onresult,
|
onresult
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
export const keymap = createKeyMap([]);
|
export const keymap = createKeyMap([]);
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
export const manager = new GraphManager(registry);
|
export const manager = new GraphManager(registry);
|
||||||
setGraphManager(manager);
|
setGraphManager(manager);
|
||||||
|
|
||||||
@@ -70,14 +67,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.on("settings", (_settings) => {
|
manager.on('settings', (_settings) => {
|
||||||
settingTypes = { ...settingTypes, ..._settings.types };
|
settingTypes = { ...settingTypes, ..._settings.types };
|
||||||
settings = _settings.values;
|
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));
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (graph) {
|
if (graph) {
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||||
import { Color, LinearSRGBColorSpace } from "three";
|
import { Color, LinearSRGBColorSpace } from 'three';
|
||||||
|
|
||||||
const variables = [
|
const variables = [
|
||||||
"layer-0",
|
'layer-0',
|
||||||
"layer-1",
|
'layer-1',
|
||||||
"layer-2",
|
'layer-2',
|
||||||
"layer-3",
|
'layer-3',
|
||||||
"outline",
|
'outline',
|
||||||
"active",
|
'active',
|
||||||
"selected",
|
'selected',
|
||||||
"edge",
|
'edge'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function getColor(variable: (typeof variables)[number]) {
|
function getColor(variable: (typeof variables)[number]) {
|
||||||
const style = getComputedStyle(document.body.parentElement!);
|
const style = getComputedStyle(document.body.parentElement!);
|
||||||
let color = style.getPropertyValue(`--${variable}`);
|
const color = style.getPropertyValue(`--color-${variable}`);
|
||||||
return new Color().setStyle(color, LinearSRGBColorSpace);
|
return new Color().setStyle(color, LinearSRGBColorSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colors = Object.fromEntries(
|
export const colors = Object.fromEntries(
|
||||||
variables.map((v) => [v, getColor(v)]),
|
variables.map((v) => [v, getColor(v)])
|
||||||
) as Record<(typeof variables)[number], Color>;
|
) as Record<(typeof variables)[number], Color>;
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!appSettings.value.theme || !("getComputedStyle" in globalThis)) return;
|
if (!appSettings.value.theme || !('getComputedStyle' in globalThis)) return;
|
||||||
const style = getComputedStyle(document.body.parentElement!);
|
const style = getComputedStyle(document.body.parentElement!);
|
||||||
for (const v of variables) {
|
for (const v of variables) {
|
||||||
const hex = style.getPropertyValue(`--${v}`);
|
const hex = style.getPropertyValue(`--color-${v}`);
|
||||||
colors[v].setStyle(hex, LinearSRGBColorSpace);
|
colors[v].setStyle(hex, LinearSRGBColorSpace);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class FileDropEventManager {
|
|||||||
constructor(
|
constructor(
|
||||||
private graph: GraphManager,
|
private graph: GraphManager,
|
||||||
private state: GraphState
|
private state: GraphState
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
handleFileDrop(event: DragEvent) {
|
handleFileDrop(event: DragEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -17,19 +17,21 @@ export class FileDropEventManager {
|
|||||||
let my = event.clientY - this.state.rect.y;
|
let my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
if (nodeId) {
|
if (nodeId) {
|
||||||
let nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
|
const nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
|
||||||
let nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
|
const nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
|
||||||
if (nodeOffsetX && nodeOffsetY) {
|
if (nodeOffsetX && nodeOffsetY) {
|
||||||
mx += parseInt(nodeOffsetX);
|
mx += parseInt(nodeOffsetX);
|
||||||
my += parseInt(nodeOffsetY);
|
my += parseInt(nodeOffsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
let props = {};
|
let props = {};
|
||||||
let rawNodeProps = event.dataTransfer.getData('data/node-props');
|
const rawNodeProps = event.dataTransfer.getData('data/node-props');
|
||||||
if (rawNodeProps) {
|
if (rawNodeProps) {
|
||||||
try {
|
try {
|
||||||
props = JSON.parse(rawNodeProps);
|
props = JSON.parse(rawNodeProps);
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
console.error('Failed to parse node dropped', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pos = this.state.projectScreenToWorld(mx, my);
|
const pos = this.state.projectScreenToWorld(mx, my);
|
||||||
@@ -48,7 +50,7 @@ export class FileDropEventManager {
|
|||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
const buffer = e.target?.result;
|
const buffer = e.target?.result;
|
||||||
if (buffer?.constructor === ArrayBuffer) {
|
if (buffer?.constructor === ArrayBuffer) {
|
||||||
const nodeType = await this.graph.registry.register(buffer);
|
const nodeType = await this.graph.registry.register(nodeId, buffer);
|
||||||
|
|
||||||
this.graph.createNode({
|
this.graph.createNode({
|
||||||
type: nodeType.id,
|
type: nodeType.id,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class EdgeInteractionManager {
|
|||||||
constructor(
|
constructor(
|
||||||
private graph: GraphManager,
|
private graph: GraphManager,
|
||||||
private state: GraphState
|
private state: GraphState
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
private MIN_DISTANCE = 3;
|
private MIN_DISTANCE = 3;
|
||||||
|
|
||||||
@@ -85,7 +85,14 @@ export class EdgeInteractionManager {
|
|||||||
const pointAy = edge.points[i].z + edge.y1;
|
const pointAy = edge.points[i].z + edge.y1;
|
||||||
const pointBx = edge.points[i + DENSITY].x + edge.x1;
|
const pointBx = edge.points[i + DENSITY].x + edge.x1;
|
||||||
const pointBy = edge.points[i + DENSITY].z + edge.y1;
|
const pointBy = edge.points[i + DENSITY].z + edge.y1;
|
||||||
const distance = distanceFromPointToSegment(pointAx, pointAy, pointBx, pointBy, mouseX, mouseY);
|
const distance = distanceFromPointToSegment(
|
||||||
|
pointAx,
|
||||||
|
pointAy,
|
||||||
|
pointBx,
|
||||||
|
pointBy,
|
||||||
|
mouseX,
|
||||||
|
mouseY
|
||||||
|
);
|
||||||
if (distance < this.MIN_DISTANCE) {
|
if (distance < this.MIN_DISTANCE) {
|
||||||
if (distance < hoveredEdgeDistance) {
|
if (distance < hoveredEdgeDistance) {
|
||||||
hoveredEdgeDistance = distance;
|
hoveredEdgeDistance = distance;
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ export class MouseEventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mx = event.clientX - this.state.rect.x;
|
const mx = event.clientX - this.state.rect.x;
|
||||||
let my = event.clientY - this.state.rect.y;
|
const my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
this.state.mouseDown = [mx, my];
|
this.state.mouseDown = [mx, my];
|
||||||
this.state.cameraDown[0] = this.state.cameraPosition[0];
|
this.state.cameraDown[0] = this.state.cameraPosition[0];
|
||||||
@@ -242,8 +242,8 @@ export class MouseEventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleWindowMouseMove(event: MouseEvent) {
|
handleWindowMouseMove(event: MouseEvent) {
|
||||||
let mx = event.clientX - this.state.rect.x;
|
const mx = event.clientX - this.state.rect.x;
|
||||||
let my = event.clientY - this.state.rect.y;
|
const my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
this.state.mousePosition = this.state.projectScreenToWorld(mx, my);
|
this.state.mousePosition = this.state.projectScreenToWorld(mx, my);
|
||||||
this.state.hoveredNodeId = this.state.getNodeIdFromEvent(event);
|
this.state.hoveredNodeId = this.state.getNodeIdFromEvent(event);
|
||||||
@@ -265,7 +265,7 @@ export class MouseEventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_socket && smallestDist < 0.9) {
|
if (_socket && smallestDist < 1.5) {
|
||||||
this.state.mousePosition = _socket.position;
|
this.state.mousePosition = _socket.position;
|
||||||
this.state.hoveredSocket = _socket;
|
this.state.hoveredSocket = _socket;
|
||||||
} else {
|
} else {
|
||||||
@@ -352,9 +352,9 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// here we are handling panning of camera
|
// here we are handling panning of camera
|
||||||
this.state.isPanning = true;
|
this.state.isPanning = true;
|
||||||
let newX = this.state.cameraDown[0]
|
const newX = this.state.cameraDown[0]
|
||||||
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
let newY = this.state.cameraDown[1]
|
const newY = this.state.cameraDown[1]
|
||||||
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
|
|
||||||
this.state.cameraPosition[0] = newX;
|
this.state.cameraPosition[0] = newX;
|
||||||
@@ -392,6 +392,7 @@ export class MouseEventManager {
|
|||||||
/ zoomRatio;
|
/ zoomRatio;
|
||||||
this.state.cameraPosition[1] = this.state.mousePosition[1]
|
this.state.cameraPosition[1] = this.state.mousePosition[1]
|
||||||
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
|
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
|
||||||
/ zoomRatio, this.state.cameraPosition[2] = newZoom;
|
/ 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 EventMap = Record<string, unknown>;
|
||||||
type EventKey<T extends EventMap> = string & keyof T;
|
type EventKey<T extends EventMap> = string & keyof T;
|
||||||
type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown;
|
type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown;
|
||||||
|
|
||||||
export default class EventEmitter<
|
export default class EventEmitter<
|
||||||
T extends EventMap = { [key: string]: unknown },
|
T extends EventMap = { [key: string]: unknown }
|
||||||
> {
|
> {
|
||||||
index = 0;
|
index = 0;
|
||||||
public eventMap: T = {} as T;
|
public eventMap: T = {} as T;
|
||||||
@@ -32,11 +32,11 @@ export default class EventEmitter<
|
|||||||
public on<K extends EventKey<T>>(
|
public on<K extends EventKey<T>>(
|
||||||
event: K,
|
event: K,
|
||||||
cb: EventReceiver<T[K]>,
|
cb: EventReceiver<T[K]>,
|
||||||
throttleTimer = 0,
|
throttleTimer = 0
|
||||||
) {
|
) {
|
||||||
if (throttleTimer > 0) cb = throttle(cb, throttleTimer);
|
if (throttleTimer > 0) cb = throttle(cb, throttleTimer);
|
||||||
const cbs = Object.assign(this.cbs, {
|
const cbs = Object.assign(this.cbs, {
|
||||||
[event]: [...(this.cbs[event] || []), cb],
|
[event]: [...(this.cbs[event] || []), cb]
|
||||||
});
|
});
|
||||||
this.cbs = cbs;
|
this.cbs = cbs;
|
||||||
|
|
||||||
@@ -54,10 +54,10 @@ export default class EventEmitter<
|
|||||||
*/
|
*/
|
||||||
public once<K extends EventKey<T>>(
|
public once<K extends EventKey<T>>(
|
||||||
event: K,
|
event: K,
|
||||||
cb: EventReceiver<T[K]>,
|
cb: EventReceiver<T[K]>
|
||||||
): () => void {
|
): () => void {
|
||||||
const cbsOnce = Object.assign(this.cbsOnce, {
|
const cbsOnce = Object.assign(this.cbsOnce, {
|
||||||
[event]: [...(this.cbsOnce[event] || []), cb],
|
[event]: [...(this.cbsOnce[event] || []), cb]
|
||||||
});
|
});
|
||||||
this.cbsOnce = cbsOnce;
|
this.cbsOnce = cbsOnce;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export function createNodePath({
|
|||||||
aspectRatio = 1
|
aspectRatio = 1
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return `M0,${cornerTop}
|
return `M0,${cornerTop}
|
||||||
${cornerTop
|
${
|
||||||
|
cornerTop
|
||||||
? ` V${cornerTop}
|
? ` V${cornerTop}
|
||||||
Q0,0 ${cornerTop * aspectRatio},0
|
Q0,0 ${cornerTop * aspectRatio},0
|
||||||
H${100 - cornerTop * aspectRatio}
|
H${100 - cornerTop * aspectRatio}
|
||||||
@@ -45,40 +46,37 @@ export function createNodePath({
|
|||||||
: ` V0
|
: ` V0
|
||||||
H100
|
H100
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
V${y - height / 2}
|
V${y - height / 2}
|
||||||
${rightBump
|
${
|
||||||
|
rightBump
|
||||||
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||||
: ` H100`
|
: ` H100`
|
||||||
}
|
}
|
||||||
${cornerBottom
|
${
|
||||||
|
cornerBottom
|
||||||
? ` V${100 - cornerBottom}
|
? ` V${100 - cornerBottom}
|
||||||
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
||||||
H${cornerBottom * aspectRatio}
|
H${cornerBottom * aspectRatio}
|
||||||
Q0,100 0,${100 - cornerBottom}
|
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}`
|
leftBump
|
||||||
|
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
|
||||||
|
y - height / 2
|
||||||
|
}`
|
||||||
: ` H0`
|
: ` H0`
|
||||||
}
|
}
|
||||||
Z`.replace(/\s+/g, ' ');
|
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
|
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
|
||||||
? globalThis.structuredClone
|
? globalThis.structuredClone
|
||||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
: (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,
|
component: A,
|
||||||
subcomponents: B
|
subcomponents: B
|
||||||
): A & B {
|
): A & B {
|
||||||
|
|||||||
@@ -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> {
|
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 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>) {
|
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||||
|
|
||||||
let store: Writable<T>;
|
let store: Writable<T>;
|
||||||
|
|
||||||
if (HAS_LOCALSTORAGE) {
|
if (HAS_LOCALSTORAGE) {
|
||||||
@@ -36,18 +35,15 @@ function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
|||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
set: store.set,
|
set: store.set,
|
||||||
update: store.update
|
update: store.update
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
||||||
|
|
||||||
if (storeIds.has(key)) return storeIds.get(key) as 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);
|
storeIds.set(key, store);
|
||||||
|
|
||||||
return store
|
return store;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { create, type Delta } from 'jsondiffpatch';
|
|||||||
import { clone } from './helpers/index.js';
|
import { clone } from './helpers/index.js';
|
||||||
|
|
||||||
const diff = create({
|
const diff = create({
|
||||||
objectHash: function (obj, index) {
|
objectHash: function(obj, index) {
|
||||||
if (obj === null) return obj;
|
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;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInstance } from "@nodarium/types";
|
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||||
import { getGraphState } from "../graph-state.svelte";
|
import type { NodeInstance } from '@nodarium/types';
|
||||||
import { T } from "@threlte/core";
|
import { T } from '@threlte/core';
|
||||||
import { type Mesh } from "three";
|
import { type Mesh } from 'three';
|
||||||
import NodeFrag from "./Node.frag";
|
import { getGraphState } from '../graph-state.svelte';
|
||||||
import NodeVert from "./Node.vert";
|
import { colors } from '../graph/colors.svelte';
|
||||||
import NodeHtml from "./NodeHTML.svelte";
|
import NodeFrag from './Node.frag';
|
||||||
import { colors } from "../graph/colors.svelte";
|
import NodeVert from './Node.vert';
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import NodeHtml from './NodeHTML.svelte';
|
||||||
|
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
|
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
const isActive = $derived(graphState.activeNodeId === node.id);
|
const isActive = $derived(graphState.activeNodeId === node.id);
|
||||||
const isSelected = $derived(graphState.selectedNodes.has(node.id));
|
const isSelected = $derived(graphState.selectedNodes.has(node.id));
|
||||||
const strokeColor = $derived(
|
const strokeColor = $derived(
|
||||||
appSettings.value.theme &&
|
appSettings.value.theme
|
||||||
(isSelected
|
&& (isSelected
|
||||||
? colors.selected
|
? colors.selected
|
||||||
: isActive
|
: isActive
|
||||||
? colors.active
|
? colors.active
|
||||||
: colors.outline),
|
: colors.outline)
|
||||||
);
|
);
|
||||||
|
|
||||||
let meshRef: Mesh | undefined = $state();
|
let meshRef: Mesh | undefined = $state();
|
||||||
@@ -55,12 +55,12 @@
|
|||||||
fragmentShader={NodeFrag}
|
fragmentShader={NodeFrag}
|
||||||
transparent
|
transparent
|
||||||
uniforms={{
|
uniforms={{
|
||||||
uColorBright: { value: colors["layer-2"] },
|
uColorBright: { value: colors['layer-2'] },
|
||||||
uColorDark: { value: colors["layer-1"] },
|
uColorDark: { value: colors['layer-1'] },
|
||||||
uStrokeColor: { value: colors.outline.clone() },
|
uStrokeColor: { value: colors.outline.clone() },
|
||||||
uStrokeWidth: { value: 1.0 },
|
uStrokeWidth: { value: 1.0 },
|
||||||
uWidth: { value: 20 },
|
uWidth: { value: 20 },
|
||||||
uHeight: { value: height },
|
uHeight: { value: height }
|
||||||
}}
|
}}
|
||||||
uniforms.uStrokeColor.value={strokeColor.clone()}
|
uniforms.uStrokeColor.value={strokeColor.clone()}
|
||||||
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInstance } from "@nodarium/types";
|
import type { NodeInstance } from '@nodarium/types';
|
||||||
import NodeHeader from "./NodeHeader.svelte";
|
import { getGraphState } from '../graph-state.svelte';
|
||||||
import NodeParameter from "./NodeParameter.svelte";
|
import NodeHeader from './NodeHeader.svelte';
|
||||||
import { getGraphState } from "../graph-state.svelte";
|
import NodeParameter from './NodeParameter.svelte';
|
||||||
|
|
||||||
let ref: HTMLDivElement;
|
let ref: HTMLDivElement;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: NodeInstance;
|
node: NodeInstance;
|
||||||
position?: "absolute" | "fixed" | "relative";
|
position?: 'absolute' | 'fixed' | 'relative';
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
inView?: boolean;
|
inView?: boolean;
|
||||||
@@ -19,11 +19,11 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
node = $bindable(),
|
node = $bindable(),
|
||||||
position = "absolute",
|
position = 'absolute',
|
||||||
isActive = false,
|
isActive = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
inView = true,
|
inView = true,
|
||||||
z = 2,
|
z = 2
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// If we dont have a random offset, all nodes becom visible at the same zoom level -> stuttering
|
// If we dont have a random offset, all nodes becom visible at the same zoom level -> stuttering
|
||||||
@@ -31,12 +31,11 @@
|
|||||||
const zLimit = 2 - zOffset;
|
const zLimit = 2 - zOffset;
|
||||||
|
|
||||||
const parameters = Object.entries(node.state?.type?.inputs || {}).filter(
|
const parameters = Object.entries(node.state?.type?.inputs || {}).filter(
|
||||||
(p) =>
|
(p) => p[1].type !== 'seed' && !('setting' in p[1]) && p[1]?.hidden !== true
|
||||||
p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ("state" in node && !node.state.ref) {
|
if ('state' in node && !node.state.ref) {
|
||||||
node.state.ref = ref;
|
node.state.ref = ref;
|
||||||
graphState?.updateNodePosition(node);
|
graphState?.updateNodePosition(node);
|
||||||
}
|
}
|
||||||
@@ -47,7 +46,7 @@
|
|||||||
class="node {position}"
|
class="node {position}"
|
||||||
class:active={isActive}
|
class:active={isActive}
|
||||||
style:--cz={z + zOffset}
|
style:--cz={z + zOffset}
|
||||||
style:display={inView && z > zLimit ? "block" : "none"}
|
style:display={inView && z > zLimit ? 'block' : 'none'}
|
||||||
class:selected={isSelected}
|
class:selected={isSelected}
|
||||||
class:out-of-view={!inView}
|
class:out-of-view={!inView}
|
||||||
data-node-id={node.id}
|
data-node-id={node.id}
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
>
|
>
|
||||||
<NodeHeader {node} />
|
<NodeHeader {node} />
|
||||||
|
|
||||||
{#each parameters as [key, value], i}
|
{#each parameters as [key, value], i (key)}
|
||||||
<NodeParameter
|
<NodeParameter
|
||||||
bind:node
|
bind:node
|
||||||
id={key}
|
id={key}
|
||||||
@@ -72,22 +71,22 @@
|
|||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
color: var(--text-color);
|
color: var(--color-text);
|
||||||
transform: translate3d(var(--nx), var(--ny), 0);
|
transform: translate3d(var(--nx), var(--ny), 0);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: calc((var(--cz) - 2.5) / 3.5);
|
opacity: calc((var(--cz) - 2.5) / 3.5);
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
--stroke: var(--outline);
|
--stroke: var(--color-outline);
|
||||||
--stroke-width: 2px;
|
--stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node.active {
|
.node.active {
|
||||||
--stroke: var(--active);
|
--stroke: var(--color-active);
|
||||||
--stroke-width: 2px;
|
--stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node.selected {
|
.node.selected {
|
||||||
--stroke: var(--selected);
|
--stroke: var(--color-selected);
|
||||||
--stroke-width: 2px;
|
--stroke-width: 2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getGraphState } from "../graph-state.svelte";
|
import type { NodeInstance } from '@nodarium/types';
|
||||||
import { createNodePath } from "../helpers/index.js";
|
import { getGraphState } from '../graph-state.svelte';
|
||||||
import type { NodeInstance } from "@nodarium/types";
|
import { createNodePath } from '../helpers/index.js';
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
|
||||||
|
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
|
|
||||||
@@ -11,52 +10,52 @@
|
|||||||
function handleMouseDown(event: MouseEvent) {
|
function handleMouseDown(event: MouseEvent) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ("state" in node) {
|
if ('state' in node) {
|
||||||
graphState.setDownSocket?.({
|
graphState.setDownSocket?.({
|
||||||
node,
|
node,
|
||||||
index: 0,
|
index: 0,
|
||||||
position: graphState.getSocketPosition?.(node, 0),
|
position: graphState.getSocketPosition?.(node, 0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cornerTop = 10;
|
const cornerTop = 10;
|
||||||
const rightBump = !!node?.state?.type?.outputs?.length;
|
const rightBump = $derived(!!node?.state?.type?.outputs?.length);
|
||||||
const aspectRatio = 0.25;
|
const aspectRatio = 0.25;
|
||||||
|
|
||||||
const path = createNodePath({
|
const path = $derived(
|
||||||
depth: 5.5,
|
createNodePath({
|
||||||
height: 34,
|
depth: 5.5,
|
||||||
y: 49,
|
height: 34,
|
||||||
cornerTop,
|
y: 49,
|
||||||
rightBump,
|
cornerTop,
|
||||||
aspectRatio,
|
rightBump,
|
||||||
});
|
aspectRatio
|
||||||
const pathHover = createNodePath({
|
})
|
||||||
depth: 8.5,
|
);
|
||||||
height: 50,
|
const pathHover = $derived(
|
||||||
y: 49,
|
createNodePath({
|
||||||
cornerTop,
|
depth: 7,
|
||||||
rightBump,
|
height: 40,
|
||||||
aspectRatio,
|
y: 49,
|
||||||
});
|
cornerTop,
|
||||||
|
rightBump,
|
||||||
|
aspectRatio
|
||||||
|
})
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
|
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if appSettings.value.nodeInterface.showNodeIds}
|
{node.type.split('/').pop()}
|
||||||
<span class="bg-white text-black! mr-2 px-1 rounded-sm opacity-30"
|
|
||||||
>{node.id}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{node.type.split("/").pop()}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="click-target"
|
class="click-target"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
></div>
|
>
|
||||||
|
</div>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
@@ -68,8 +67,7 @@
|
|||||||
--hover-path: path("${pathHover}");
|
--hover-path: path("${pathHover}");
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"
|
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"></path>
|
||||||
></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,13 +108,14 @@
|
|||||||
|
|
||||||
svg path {
|
svg path {
|
||||||
stroke-width: 0.2px;
|
stroke-width: 0.2px;
|
||||||
transition:
|
transition: d 0.3s ease, fill 0.3s ease;
|
||||||
d 0.3s ease,
|
fill: var(--color-layer-2);
|
||||||
fill 0.3s ease;
|
|
||||||
fill: var(--layer-2);
|
|
||||||
stroke: var(--stroke);
|
stroke: var(--stroke);
|
||||||
stroke-width: var(--stroke-width);
|
stroke-width: var(--stroke-width);
|
||||||
d: var(--path);
|
d: var(--path);
|
||||||
|
|
||||||
|
stroke-linejoin: round;
|
||||||
|
shape-rendering: geometricPrecision;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInstance, NodeInput } from "@nodarium/types";
|
import type { NodeInput, NodeInstance } from '@nodarium/types';
|
||||||
import { Input } from "@nodarium/ui";
|
import { Input } from '@nodarium/ui';
|
||||||
import type { GraphManager } from "../graph-manager.svelte";
|
import type { GraphManager } from '../graph-manager.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: NodeInstance;
|
node: NodeInstance;
|
||||||
@@ -16,17 +16,18 @@
|
|||||||
input,
|
input,
|
||||||
id,
|
id,
|
||||||
elementId = `input-${Math.random().toString(36).substring(7)}`,
|
elementId = `input-${Math.random().toString(36).substring(7)}`,
|
||||||
graph,
|
graph
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
function getDefaultValue() {
|
function getDefaultValue() {
|
||||||
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
|
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;
|
return input?.value as number;
|
||||||
if (input.type === "boolean") return 0;
|
}
|
||||||
if (input.type === "float") return 0.5;
|
if (input.type === 'boolean') return 0;
|
||||||
if (input.type === "integer") return 0;
|
if (input.type === 'float') return 0.5;
|
||||||
if (input.type === "select") return 0;
|
if (input.type === 'integer') return 0;
|
||||||
|
if (input.type === 'select') return 0;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInput, NodeInstance } from "@nodarium/types";
|
import type { NodeInput, NodeInstance } from '@nodarium/types';
|
||||||
import { createNodePath } from "../helpers";
|
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||||
import NodeInputEl from "./NodeInput.svelte";
|
import { createNodePath } from '../helpers';
|
||||||
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
import NodeInputEl from './NodeInput.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: NodeInstance;
|
node: NodeInstance;
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
let { node = $bindable(), input, id, isLast }: Props = $props();
|
let { node = $bindable(), input, id, isLast }: Props = $props();
|
||||||
|
|
||||||
const inputType = node?.state?.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 graphState = getGraphState();
|
||||||
const graphId = graph?.id;
|
const graphId = graph?.id;
|
||||||
@@ -30,71 +30,65 @@
|
|||||||
graphState.setDownSocket({
|
graphState.setDownSocket({
|
||||||
node,
|
node,
|
||||||
index: id,
|
index: id,
|
||||||
position: graphState.getSocketPosition?.(node, id),
|
position: graphState.getSocketPosition?.(node, id)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const leftBump = node.state?.type?.inputs?.[id].internal !== true;
|
const leftBump = $derived(node.state?.type?.inputs?.[id].internal !== true);
|
||||||
const cornerBottom = isLast ? 5 : 0;
|
const cornerBottom = $derived(isLast ? 5 : 0);
|
||||||
const aspectRatio = 0.5;
|
const aspectRatio = 0.5;
|
||||||
|
|
||||||
const path = createNodePath({
|
const path = $derived(
|
||||||
depth: 7,
|
createNodePath({
|
||||||
height: 20,
|
depth: 6,
|
||||||
y: 50.5,
|
height: 18,
|
||||||
cornerBottom,
|
y: 50.5,
|
||||||
leftBump,
|
cornerBottom,
|
||||||
aspectRatio,
|
leftBump,
|
||||||
});
|
aspectRatio
|
||||||
const pathDisabled = createNodePath({
|
})
|
||||||
depth: 6,
|
);
|
||||||
height: 18,
|
const pathHover = $derived(
|
||||||
y: 50.5,
|
createNodePath({
|
||||||
cornerBottom,
|
depth: 7,
|
||||||
leftBump,
|
height: 20,
|
||||||
aspectRatio,
|
y: 50.5,
|
||||||
});
|
cornerBottom,
|
||||||
const pathHover = createNodePath({
|
leftBump,
|
||||||
depth: 8,
|
aspectRatio
|
||||||
height: 25,
|
})
|
||||||
y: 50.5,
|
);
|
||||||
cornerBottom,
|
|
||||||
leftBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
data-node-type={node.type}
|
data-node-type={node.type}
|
||||||
data-node-input={id}
|
data-node-input={id}
|
||||||
class:disabled={!graphState?.possibleSocketIds.has(socketId)}
|
class:possible-socket={graphState?.possibleSocketIds.has(socketId)}
|
||||||
>
|
>
|
||||||
{#key id && graphId}
|
{#key id && graphId}
|
||||||
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
|
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
|
||||||
{#if inputType.label !== ""}
|
{#if inputType?.label !== ''}
|
||||||
<label for={elementId} title={input.description}
|
<label for={elementId} title={input.description}>{input.label || id}</label>
|
||||||
>{input.label || id}</label
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
|
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
|
||||||
title={JSON.stringify(input, null, 2)}
|
title={JSON.stringify(input, null, 2)}
|
||||||
></span>
|
></span>
|
||||||
{#if inputType.external !== true}
|
{#if inputType?.external !== true}
|
||||||
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
|
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if node?.state?.type?.inputs?.[id]?.internal !== true}
|
{#if node?.state?.type?.inputs?.[id]?.internal !== true}
|
||||||
<div data-node-socket class="large target"></div>
|
|
||||||
<div
|
<div
|
||||||
data-node-socket
|
data-node-socket
|
||||||
class="small target"
|
class="target"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
></div>
|
>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
@@ -107,7 +101,6 @@
|
|||||||
style={`
|
style={`
|
||||||
--path: path("${path}");
|
--path: path("${path}");
|
||||||
--hover-path: path("${pathHover}");
|
--hover-path: path("${pathHover}");
|
||||||
--hover-path-disabled: path("${pathDisabled}");
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<path vector-effect="non-scaling-stroke"></path>
|
<path vector-effect="non-scaling-stroke"></path>
|
||||||
@@ -123,28 +116,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.target {
|
.target {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%) translateX(-50%);
|
transform: translateY(-50%) translateX(-50%);
|
||||||
/* background: red; */
|
|
||||||
/* opacity: 0.1; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.small.target {
|
.possible-socket .target {
|
||||||
width: 30px;
|
box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.5);
|
||||||
height: 30px;
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
z-index: -10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large.target {
|
.target:hover ~ svg path{
|
||||||
width: 60px;
|
d: var(--hover-path);
|
||||||
height: 60px;
|
|
||||||
cursor: unset;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.hovering-sockets) .large.target {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -169,26 +156,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg path {
|
svg path {
|
||||||
transition:
|
transition: d 0.3s ease, fill 0.3s ease;
|
||||||
d 0.3s ease,
|
fill: var(--color-layer-1);
|
||||||
fill 0.3s ease;
|
|
||||||
fill: var(--layer-1);
|
|
||||||
stroke: var(--stroke);
|
stroke: var(--stroke);
|
||||||
stroke-width: var(--stroke-width);
|
stroke-width: var(--stroke-width);
|
||||||
d: var(--path);
|
d: var(--path);
|
||||||
}
|
|
||||||
|
|
||||||
:global {
|
stroke-linejoin: round;
|
||||||
.hovering-sockets .large:hover ~ svg path {
|
shape-rendering: geometricPrecision;
|
||||||
d: var(--hover-path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content.disabled {
|
.content.disabled {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled svg path {
|
.possible-socket svg path {
|
||||||
d: var(--hover-path-disabled) !important;
|
d: var(--hover-path);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
86
app/src/lib/graph-interface/test-utils.ts
Normal file
86
app/src/lib/graph-interface/test-utils.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { NodeDefinition, NodeId, NodeRegistry } from '@nodarium/types';
|
||||||
|
|
||||||
|
export function createMockNodeRegistry(nodes: NodeDefinition[]): NodeRegistry {
|
||||||
|
const nodesMap = new Map(nodes.map(n => [n.id, n]));
|
||||||
|
return {
|
||||||
|
status: 'ready' as const,
|
||||||
|
load: async (nodeIds: NodeId[]) => {
|
||||||
|
const loaded: NodeDefinition[] = [];
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
if (nodesMap.has(id)) {
|
||||||
|
loaded.push(nodesMap.get(id)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loaded;
|
||||||
|
},
|
||||||
|
getNode: (id: string) => nodesMap.get(id as NodeId),
|
||||||
|
getAllNodes: () => Array.from(nodesMap.values()),
|
||||||
|
register: async () => {
|
||||||
|
throw new Error('Not implemented in mock');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mockFloatOutputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/output',
|
||||||
|
inputs: {},
|
||||||
|
outputs: ['float'],
|
||||||
|
meta: { title: 'Float Output' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockFloatInputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/input',
|
||||||
|
inputs: { value: { type: 'float' } },
|
||||||
|
outputs: [],
|
||||||
|
meta: { title: 'Float Input' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockGeometryOutputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/geometry',
|
||||||
|
inputs: {},
|
||||||
|
outputs: ['geometry'],
|
||||||
|
meta: { title: 'Geometry Output' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockPathInputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/path',
|
||||||
|
inputs: { input: { type: 'path', accepts: ['geometry'] } },
|
||||||
|
outputs: [],
|
||||||
|
meta: { title: 'Path Input' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockVec3OutputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/vec3',
|
||||||
|
inputs: {},
|
||||||
|
outputs: ['vec3'],
|
||||||
|
meta: { title: 'Vec3 Output' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockIntegerInputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/integer',
|
||||||
|
inputs: { value: { type: 'integer' } },
|
||||||
|
outputs: [],
|
||||||
|
meta: { title: 'Integer Input' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockBooleanOutputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/boolean',
|
||||||
|
inputs: {},
|
||||||
|
outputs: ['boolean'],
|
||||||
|
meta: { title: 'Boolean Output' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockBooleanInputNode: NodeDefinition = {
|
||||||
|
id: 'test/node/boolean-input',
|
||||||
|
inputs: { value: { type: 'boolean' } },
|
||||||
|
outputs: [],
|
||||||
|
meta: { title: 'Boolean Input' },
|
||||||
|
execute: () => new Int32Array()
|
||||||
|
};
|
||||||
110
app/src/lib/graph-templates.test.ts
Normal file
110
app/src/lib/graph-templates.test.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { grid } from '$lib/graph-templates/grid';
|
||||||
|
import { tree } from '$lib/graph-templates/tree';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('graph-templates', () => {
|
||||||
|
describe('grid', () => {
|
||||||
|
it('should create a grid graph with nodes and edges', () => {
|
||||||
|
const result = grid(2, 3);
|
||||||
|
expect(result.nodes.length).toBeGreaterThan(0);
|
||||||
|
expect(result.edges.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have output node at the end', () => {
|
||||||
|
const result = grid(1, 1);
|
||||||
|
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||||
|
expect(outputNode).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes based on grid dimensions', () => {
|
||||||
|
const result = grid(2, 2);
|
||||||
|
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
|
||||||
|
expect(mathNodes.length).toBeGreaterThan(0);
|
||||||
|
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||||
|
expect(outputNode).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have output node at the end', () => {
|
||||||
|
const result = grid(1, 1);
|
||||||
|
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||||
|
expect(outputNode).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes based on grid dimensions', () => {
|
||||||
|
const result = grid(2, 2);
|
||||||
|
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
|
||||||
|
expect(mathNodes.length).toBeGreaterThan(0);
|
||||||
|
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||||
|
expect(outputNode).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid node positions', () => {
|
||||||
|
const result = grid(3, 2);
|
||||||
|
|
||||||
|
result.nodes.forEach(node => {
|
||||||
|
expect(node.position).toHaveLength(2);
|
||||||
|
expect(typeof node.position[0]).toBe('number');
|
||||||
|
expect(typeof node.position[1]).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate valid graph structure', () => {
|
||||||
|
const result = grid(2, 2);
|
||||||
|
|
||||||
|
result.nodes.forEach(node => {
|
||||||
|
expect(typeof node.id).toBe('number');
|
||||||
|
expect(node.type).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
result.edges.forEach(edge => {
|
||||||
|
expect(edge).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tree', () => {
|
||||||
|
it('should create a tree graph with specified depth', () => {
|
||||||
|
const result = tree(0);
|
||||||
|
|
||||||
|
expect(result.nodes.length).toBeGreaterThan(0);
|
||||||
|
expect(result.edges.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have root output node', () => {
|
||||||
|
const result = tree(2);
|
||||||
|
|
||||||
|
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||||
|
expect(outputNode).toBeDefined();
|
||||||
|
expect(outputNode?.id).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increase node count with depth', () => {
|
||||||
|
const tree0 = tree(0);
|
||||||
|
const tree1 = tree(1);
|
||||||
|
const tree2 = tree(2);
|
||||||
|
|
||||||
|
expect(tree0.nodes.length).toBeLessThan(tree1.nodes.length);
|
||||||
|
expect(tree1.nodes.length).toBeLessThan(tree2.nodes.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create binary tree structure', () => {
|
||||||
|
const result = tree(2);
|
||||||
|
|
||||||
|
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
|
||||||
|
expect(mathNodes.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const edgeCount = result.edges.length;
|
||||||
|
expect(edgeCount).toBe(result.nodes.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid node positions', () => {
|
||||||
|
const result = tree(3);
|
||||||
|
|
||||||
|
result.nodes.forEach(node => {
|
||||||
|
expect(node.position).toHaveLength(2);
|
||||||
|
expect(typeof node.position[0]).toBe('number');
|
||||||
|
expect(typeof node.position[1]).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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) {
|
export function grid(width: number, height: number) {
|
||||||
|
|
||||||
const graph: Graph = {
|
const graph: Graph = {
|
||||||
id: Math.floor(Math.random() * 100000),
|
id: Math.floor(Math.random() * 100000),
|
||||||
edges: [],
|
edges: [],
|
||||||
nodes: [],
|
nodes: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const amount = width * height;
|
const amount = width * height;
|
||||||
@@ -18,19 +17,18 @@ export function grid(width: number, height: number) {
|
|||||||
id: i,
|
id: i,
|
||||||
position: [x * 30, y * 40],
|
position: [x * 30, y * 40],
|
||||||
props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 },
|
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({
|
graph.nodes.push({
|
||||||
id: amount,
|
id: amount,
|
||||||
position: [width * 30, (height - 1) * 40],
|
position: [width * 30, (height - 1) * 40],
|
||||||
type: "max/plantarium/output",
|
type: 'max/plantarium/output',
|
||||||
props: {},
|
props: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export { grid } from "./grid";
|
export { default as defaultPlant } from './default.json';
|
||||||
export { tree } from "./tree";
|
export { grid } from './grid';
|
||||||
export { plant } from "./plant";
|
export { default as lottaFaces } from './lotta-faces.json';
|
||||||
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 { default as lottaNodes } from './lotta-nodes.json';
|
||||||
export { default as defaultPlant } from "./default.json"
|
export { plant } from './plant';
|
||||||
export { default as lottaNodesAndFaces } from "./lotta-nodes-and-faces.json";
|
export { default as simple } from './simple.json';
|
||||||
|
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 = {
|
export const plant = {
|
||||||
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
|
'settings': { 'resolution.circle': 26, 'resolution.curve': 39 },
|
||||||
"nodes": [
|
'nodes': [
|
||||||
{ "id": 9, "position": [180, 80], "type": "max/plantarium/output", "props": {} },
|
{ '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': 10,
|
||||||
{ "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 } },
|
'position': [55, 80],
|
||||||
{ "id": 13, "position": [130, 80], "type": "max/plantarium/noise", "props": { "strength": 8, "scale": 7.7, "fixBottom": 1, "directionalStrength": [1, 0, 1], "depth": 1 } },
|
'type': 'max/plantarium/stem',
|
||||||
{ "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 } }
|
'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']
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|||||||
63
app/src/lib/graph-templates/simple.json
Normal file
63
app/src/lib/graph-templates/simple.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"settings": {
|
||||||
|
"resolution.circle": 54,
|
||||||
|
"resolution.curve": 20,
|
||||||
|
"randomSeed": true
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"title": "New Project",
|
||||||
|
"lastModified": "2026-02-03T16:56:40.375Z"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"position": [
|
||||||
|
215,
|
||||||
|
85
|
||||||
|
],
|
||||||
|
"type": "max/plantarium/output",
|
||||||
|
"props": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"position": [
|
||||||
|
165,
|
||||||
|
72.5
|
||||||
|
],
|
||||||
|
"type": "max/plantarium/stem",
|
||||||
|
"props": {
|
||||||
|
"amount": 50,
|
||||||
|
"length": 4,
|
||||||
|
"thickness": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"position": [
|
||||||
|
190,
|
||||||
|
77.5
|
||||||
|
],
|
||||||
|
"type": "max/plantarium/noise",
|
||||||
|
"props": {
|
||||||
|
"plant": 0,
|
||||||
|
"scale": 0.5,
|
||||||
|
"strength": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
[
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
11,
|
||||||
|
"plant"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11,
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
"input"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,28 +1,26 @@
|
|||||||
import type { Graph, SerializedNode } from "@nodarium/types";
|
import type { Graph, SerializedNode } from '@nodarium/types';
|
||||||
|
|
||||||
export function tree(depth: number): Graph {
|
export function tree(depth: number): Graph {
|
||||||
|
|
||||||
const nodes: SerializedNode[] = [
|
const nodes: SerializedNode[] = [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
type: "max/plantarium/output",
|
type: 'max/plantarium/output',
|
||||||
position: [0, 0]
|
position: [0, 0]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
type: "max/plantarium/math",
|
type: 'max/plantarium/math',
|
||||||
position: [-40, -10]
|
position: [-40, -10]
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
const edges: [number, number, number, string][] = [
|
const edges: [number, number, number, string][] = [
|
||||||
[1, 0, 0, "input"]
|
[1, 0, 0, 'input']
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let d = 0; d < depth; d++) {
|
for (let d = 0; d < depth; d++) {
|
||||||
const amount = Math.pow(2, d);
|
const amount = Math.pow(2, d);
|
||||||
for (let i = 0; i < amount; i++) {
|
for (let i = 0; i < amount; i++) {
|
||||||
|
|
||||||
const id0 = amount * 2 + i * 2;
|
const id0 = amount * 2 + i * 2;
|
||||||
const id1 = amount * 2 + i * 2 + 1;
|
const id1 = amount * 2 + i * 2 + 1;
|
||||||
|
|
||||||
@@ -33,24 +31,22 @@ export function tree(depth: number): Graph {
|
|||||||
|
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: id0,
|
id: id0,
|
||||||
type: "max/plantarium/math",
|
type: 'max/plantarium/math',
|
||||||
position: [x, y],
|
position: [x, y]
|
||||||
});
|
});
|
||||||
edges.push([id0, 0, parent, "a"]);
|
edges.push([id0, 0, parent, 'a']);
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: id1,
|
id: id1,
|
||||||
type: "max/plantarium/math",
|
type: 'max/plantarium/math',
|
||||||
position: [x, y + 35],
|
position: [x, y + 35]
|
||||||
});
|
});
|
||||||
edges.push([id1, 0, parent, "b"]);
|
edges.push([id1, 0, parent, 'b']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Math.floor(Math.random() * 100000),
|
id: Math.floor(Math.random() * 100000),
|
||||||
nodes,
|
nodes,
|
||||||
edges
|
edges
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, type Snippet } from "svelte";
|
import { getContext, type Snippet } from 'svelte';
|
||||||
|
|
||||||
let index = $state(-1);
|
let index = $state(-1);
|
||||||
let wrapper: HTMLDivElement;
|
let wrapper: HTMLDivElement;
|
||||||
@@ -8,19 +8,17 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = getContext<() => number>("registerCell")();
|
index = getContext<() => number>('registerCell')();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sizes = getContext<{ value: string[] }>("sizes");
|
const sizes = getContext<{ value: string[] }>('sizes');
|
||||||
|
|
||||||
let downSizes: string[] = [];
|
|
||||||
let downWidth = 0;
|
let downWidth = 0;
|
||||||
let mouseDown = false;
|
let mouseDown = false;
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
|
|
||||||
function handleMouseDown(event: MouseEvent) {
|
function handleMouseDown(event: MouseEvent) {
|
||||||
downSizes = [...sizes.value];
|
|
||||||
mouseDown = true;
|
mouseDown = true;
|
||||||
startX = event.clientX;
|
startX = event.clientX;
|
||||||
downWidth = wrapper.getBoundingClientRect().width;
|
downWidth = wrapper.getBoundingClientRect().width;
|
||||||
@@ -45,7 +43,8 @@
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
></div>
|
>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="cell" bind:this={wrapper}>
|
<div class="cell" bind:this={wrapper}>
|
||||||
@@ -63,7 +62,7 @@
|
|||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: var(--outline);
|
background: var(--color-outline);
|
||||||
}
|
}
|
||||||
.seperator::before {
|
.seperator::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { setContext, type Snippet } from "svelte";
|
import { onMount, setContext, type Snippet } from 'svelte';
|
||||||
|
|
||||||
const { children, id } = $props<{ children?: Snippet; id?: string }>();
|
const { children, id } = $props<{ children?: Snippet; id?: string }>();
|
||||||
|
|
||||||
setContext("grid-id", id);
|
onMount(() => {
|
||||||
|
setContext('grid-id', id);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children({ id })}
|
{@render children({ id })}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script lang="ts">
|
<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, []);
|
let sizes = localState<string[]>(gridId, []);
|
||||||
|
|
||||||
const { children } = $props();
|
const { children } = $props();
|
||||||
|
|
||||||
let registerIndex = 0;
|
let registerIndex = 0;
|
||||||
setContext("registerCell", function () {
|
setContext('registerCell', function() {
|
||||||
let index = registerIndex;
|
let index = registerIndex;
|
||||||
registerIndex++;
|
registerIndex++;
|
||||||
if (registerIndex > sizes.value.length) {
|
if (registerIndex > sizes.value.length) {
|
||||||
sizes.value = [...sizes.value, "1fr"];
|
sizes.value = [...sizes.value, '1fr'];
|
||||||
}
|
}
|
||||||
return index;
|
return index;
|
||||||
});
|
});
|
||||||
|
|
||||||
setContext("sizes", sizes);
|
setContext('sizes', sizes);
|
||||||
|
|
||||||
const cols = $derived(
|
const cols = $derived(
|
||||||
sizes.value.map((size, i) => `${i > 0 ? "1px " : ""}` + size).join(" "),
|
sizes.value.map((size, i) => `${i > 0 ? '1px ' : ''}` + size).join(' ')
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { withSubComponents } from "$lib/helpers";
|
import { withSubComponents } from '$lib/helpers';
|
||||||
import Grid from "./Grid.svelte";
|
import Cell from './Cell.svelte';
|
||||||
import Row from "./Row.svelte";
|
import Grid from './Grid.svelte';
|
||||||
import Cell from "./Cell.svelte";
|
import Row from './Row.svelte';
|
||||||
|
|
||||||
export default withSubComponents(Grid, { Row, Cell });
|
export default withSubComponents(Grid, { Row, Cell });
|
||||||
|
|||||||
145
app/src/lib/helpers.test.ts
Normal file
145
app/src/lib/helpers.test.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { clone, debounce, humanizeDuration, humanizeNumber, lerp, snapToGrid } from '$lib/helpers';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('helpers', () => {
|
||||||
|
describe('snapToGrid', () => {
|
||||||
|
it('should snap to nearest grid point', () => {
|
||||||
|
expect(snapToGrid(5, 10)).toBe(10);
|
||||||
|
expect(snapToGrid(15, 10)).toBe(20);
|
||||||
|
expect(snapToGrid(0, 10)).toBe(0);
|
||||||
|
expect(snapToGrid(-10, 10)).toBe(-10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should snap exact midpoint values', () => {
|
||||||
|
expect(snapToGrid(5, 10)).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default grid size of 10', () => {
|
||||||
|
expect(snapToGrid(5)).toBe(10);
|
||||||
|
expect(snapToGrid(15)).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle values exactly on grid', () => {
|
||||||
|
expect(snapToGrid(10, 10)).toBe(10);
|
||||||
|
expect(snapToGrid(20, 10)).toBe(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lerp', () => {
|
||||||
|
it('should linearly interpolate between two values', () => {
|
||||||
|
expect(lerp(0, 100, 0)).toBe(0);
|
||||||
|
expect(lerp(0, 100, 0.5)).toBe(50);
|
||||||
|
expect(lerp(0, 100, 1)).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle negative values', () => {
|
||||||
|
expect(lerp(-50, 50, 0.5)).toBe(0);
|
||||||
|
expect(lerp(-100, 0, 0.5)).toBe(-50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle t values outside 0-1 range', () => {
|
||||||
|
expect(lerp(0, 100, -0.5)).toBe(-50);
|
||||||
|
expect(lerp(0, 100, 1.5)).toBe(150);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('humanizeNumber', () => {
|
||||||
|
it('should return unchanged numbers below 1000', () => {
|
||||||
|
expect(humanizeNumber(0)).toBe('0');
|
||||||
|
expect(humanizeNumber(999)).toBe('999');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add K suffix for thousands', () => {
|
||||||
|
expect(humanizeNumber(1000)).toBe('1K');
|
||||||
|
expect(humanizeNumber(1500)).toBe('1.5K');
|
||||||
|
expect(humanizeNumber(999999)).toBe('1000K');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add M suffix for millions', () => {
|
||||||
|
expect(humanizeNumber(1000000)).toBe('1M');
|
||||||
|
expect(humanizeNumber(2500000)).toBe('2.5M');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add B suffix for billions', () => {
|
||||||
|
expect(humanizeNumber(1000000000)).toBe('1B');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('humanizeDuration', () => {
|
||||||
|
it('should return ms for very short durations', () => {
|
||||||
|
expect(humanizeDuration(100)).toBe('100ms');
|
||||||
|
expect(humanizeDuration(999)).toBe('999ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format seconds', () => {
|
||||||
|
expect(humanizeDuration(1000)).toBe('1s');
|
||||||
|
expect(humanizeDuration(1500)).toBe('1s500ms');
|
||||||
|
expect(humanizeDuration(59000)).toBe('59s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format minutes', () => {
|
||||||
|
expect(humanizeDuration(60000)).toBe('1m');
|
||||||
|
expect(humanizeDuration(90000)).toBe('1m 30s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format hours', () => {
|
||||||
|
expect(humanizeDuration(3600000)).toBe('1h');
|
||||||
|
expect(humanizeDuration(3661000)).toBe('1h 1m 1s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format days', () => {
|
||||||
|
expect(humanizeDuration(86400000)).toBe('1d');
|
||||||
|
expect(humanizeDuration(90061000)).toBe('1d 1h 1m 1s');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle zero', () => {
|
||||||
|
expect(humanizeDuration(0)).toBe('0ms');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('debounce', () => {
|
||||||
|
it('should return a function', () => {
|
||||||
|
const fn = debounce(() => {}, 100);
|
||||||
|
expect(typeof fn).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only call once when invoked multiple times within delay', () => {
|
||||||
|
let callCount = 0;
|
||||||
|
const fn = debounce(() => {
|
||||||
|
callCount++;
|
||||||
|
}, 100);
|
||||||
|
fn();
|
||||||
|
const firstCall = callCount;
|
||||||
|
fn();
|
||||||
|
fn();
|
||||||
|
expect(callCount).toBe(firstCall);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clone', () => {
|
||||||
|
it('should deep clone objects', () => {
|
||||||
|
const original = { a: 1, b: { c: 2 } };
|
||||||
|
const cloned = clone(original);
|
||||||
|
|
||||||
|
expect(cloned).toEqual(original);
|
||||||
|
expect(cloned).not.toBe(original);
|
||||||
|
expect(cloned.b).not.toBe(original.b);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays', () => {
|
||||||
|
const original = [1, 2, [3, 4]];
|
||||||
|
const cloned = clone(original);
|
||||||
|
|
||||||
|
expect(cloned).toEqual(original);
|
||||||
|
expect(cloned).not.toBe(original);
|
||||||
|
expect(cloned[2]).not.toBe(original[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle primitives', () => {
|
||||||
|
expect(clone(42)).toBe(42);
|
||||||
|
expect(clone('hello')).toBe('hello');
|
||||||
|
expect(clone(true)).toBe(true);
|
||||||
|
expect(clone(null)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,38 +1,39 @@
|
|||||||
import { derived, get, writable } from "svelte/store";
|
import { derived, get, writable } from 'svelte/store';
|
||||||
|
|
||||||
type Shortcut = {
|
export type ShortCut = {
|
||||||
key: string | string[],
|
key: string | string[];
|
||||||
shift?: boolean,
|
shift?: boolean;
|
||||||
ctrl?: boolean,
|
ctrl?: boolean;
|
||||||
alt?: boolean,
|
alt?: boolean;
|
||||||
preventDefault?: boolean,
|
preventDefault?: boolean;
|
||||||
description?: string,
|
description?: string;
|
||||||
callback: (event: KeyboardEvent) => void
|
callback: (event: KeyboardEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getShortcutId(shortcut: ShortCut) {
|
||||||
|
return `${shortcut.key}${shortcut.shift ? '+shift' : ''}${shortcut.ctrl ? '+ctrl' : ''}${
|
||||||
|
shortcut.alt ? '+alt' : ''
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShortcutId(shortcut: Shortcut) {
|
export function createKeyMap(keys: ShortCut[]) {
|
||||||
return `${shortcut.key}${shortcut.shift ? "+shift" : ""}${shortcut.ctrl ? "+ctrl" : ""}${shortcut.alt ? "+alt" : ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createKeyMap(keys: Shortcut[]) {
|
|
||||||
|
|
||||||
const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
|
const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleKeyboardEvent: (event: KeyboardEvent) => {
|
handleKeyboardEvent: (event: KeyboardEvent) => {
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
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 => {
|
const key = [...get(store).values()].find(k => {
|
||||||
if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false;
|
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 ('shift' in k && k.shift !== event.shiftKey) return false;
|
||||||
if ("ctrl" in k && k.ctrl !== event.ctrlKey) return false;
|
if ('ctrl' in k && k.ctrl !== event.ctrlKey) return false;
|
||||||
if ("alt" in k && k.alt !== event.altKey) return false;
|
if ('alt' in k && k.alt !== event.altKey) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (key && key.preventDefault) event.preventDefault();
|
if (key && key.preventDefault) event.preventDefault();
|
||||||
key?.callback(event);
|
key?.callback(event);
|
||||||
},
|
},
|
||||||
addShortcut: (shortcut: Shortcut) => {
|
addShortcut: (shortcut: ShortCut) => {
|
||||||
if (Array.isArray(shortcut.key)) {
|
if (Array.isArray(shortcut.key)) {
|
||||||
for (const k of shortcut.key) {
|
for (const k of shortcut.key) {
|
||||||
store.update(shortcuts => {
|
store.update(shortcuts => {
|
||||||
@@ -52,6 +53,5 @@ export function createKeyMap(keys: Shortcut[]) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keys: derived(store, $store => Array.from($store.values()))
|
keys: derived(store, $store => Array.from($store.values()))
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
72
app/src/lib/helpers/deepMerge.test.ts
Normal file
72
app/src/lib/helpers/deepMerge.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { isObject, mergeDeep } from '$lib/helpers/deepMerge';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('deepMerge', () => {
|
||||||
|
describe('isObject', () => {
|
||||||
|
it('should return true for plain objects', () => {
|
||||||
|
expect(isObject({})).toBe(true);
|
||||||
|
expect(isObject({ a: 1 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-objects', () => {
|
||||||
|
expect(isObject([])).toBe(false);
|
||||||
|
expect(isObject('string')).toBe(false);
|
||||||
|
expect(isObject(42)).toBe(false);
|
||||||
|
expect(isObject(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mergeDeep', () => {
|
||||||
|
it('should merge two flat objects', () => {
|
||||||
|
const target = { a: 1, b: 2 };
|
||||||
|
const source = { b: 3, c: 4 };
|
||||||
|
const result = mergeDeep(target, source);
|
||||||
|
|
||||||
|
expect(result).toEqual({ a: 1, b: 3, c: 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deeply merge nested objects', () => {
|
||||||
|
const target = { a: { x: 1 }, b: { y: 2 } };
|
||||||
|
const source = { a: { y: 2 }, c: { z: 3 } };
|
||||||
|
const result = mergeDeep(target, source);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
a: { x: 1, y: 2 },
|
||||||
|
b: { y: 2 },
|
||||||
|
c: { z: 3 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple sources', () => {
|
||||||
|
const target = { a: 1 };
|
||||||
|
const source1 = { b: 2 };
|
||||||
|
const source2 = { c: 3 };
|
||||||
|
const result = mergeDeep(target, source1, source2);
|
||||||
|
|
||||||
|
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return target if no sources provided', () => {
|
||||||
|
const target = { a: 1 };
|
||||||
|
const result = mergeDeep(target);
|
||||||
|
|
||||||
|
expect(result).toBe(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite non-object values', () => {
|
||||||
|
const target = { a: { b: 1 } };
|
||||||
|
const source = { a: 'string' };
|
||||||
|
const result = mergeDeep(target, source);
|
||||||
|
|
||||||
|
expect(result.a).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays by replacing', () => {
|
||||||
|
const target = { a: [1, 2] };
|
||||||
|
const source = { a: [3, 4] };
|
||||||
|
const result = mergeDeep(target, source);
|
||||||
|
|
||||||
|
expect(result.a).toEqual([3, 4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,7 +18,7 @@ export function animate(duration: number, callback: (progress: number) => void |
|
|||||||
} else {
|
} else {
|
||||||
callback(1);
|
callback(1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +30,11 @@ export function createNodePath({
|
|||||||
cornerBottom = 0,
|
cornerBottom = 0,
|
||||||
leftBump = false,
|
leftBump = false,
|
||||||
rightBump = false,
|
rightBump = false,
|
||||||
aspectRatio = 1,
|
aspectRatio = 1
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return `M0,${cornerTop}
|
return `M0,${cornerTop}
|
||||||
${cornerTop
|
${
|
||||||
|
cornerTop
|
||||||
? ` V${cornerTop}
|
? ` V${cornerTop}
|
||||||
Q0,0 ${cornerTop * aspectRatio},0
|
Q0,0 ${cornerTop * aspectRatio},0
|
||||||
H${100 - cornerTop * aspectRatio}
|
H${100 - cornerTop * aspectRatio}
|
||||||
@@ -42,40 +43,45 @@ export function createNodePath({
|
|||||||
: ` V0
|
: ` V0
|
||||||
H100
|
H100
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
V${y - height / 2}
|
V${y - height / 2}
|
||||||
${rightBump
|
${
|
||||||
|
rightBump
|
||||||
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||||
: ` H100`
|
: ` H100`
|
||||||
}
|
}
|
||||||
${cornerBottom
|
${
|
||||||
|
cornerBottom
|
||||||
? ` V${100 - cornerBottom}
|
? ` V${100 - cornerBottom}
|
||||||
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
||||||
H${cornerBottom * aspectRatio}
|
H${cornerBottom * aspectRatio}
|
||||||
Q0,100 0,${100 - cornerBottom}
|
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}`
|
leftBump
|
||||||
|
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
|
||||||
|
y - height / 2
|
||||||
|
}`
|
||||||
: ` H0`
|
: ` 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>;
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
return function (this: any, ...args: any[]) {
|
return function(this: unknown, ...args: unknown[]) {
|
||||||
clearTimeout(timeoutId);
|
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, unknown>>(
|
||||||
|
|
||||||
export function withSubComponents<A, B extends Record<string, any>>(
|
|
||||||
component: A,
|
component: A,
|
||||||
subcomponents: B
|
subcomponents: B
|
||||||
): A & B {
|
): A & B {
|
||||||
@@ -87,7 +93,7 @@ export function withSubComponents<A, B extends Record<string, any>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function humanizeNumber(number: number): string {
|
export function humanizeNumber(number: number): string {
|
||||||
const suffixes = ["", "K", "M", "B", "T"];
|
const suffixes = ['', 'K', 'M', 'B', 'T'];
|
||||||
if (number < 1000) {
|
if (number < 1000) {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
@@ -104,11 +110,15 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
|||||||
const millisecondsPerHour = 3600000;
|
const millisecondsPerHour = 3600000;
|
||||||
const millisecondsPerDay = 86400000;
|
const millisecondsPerDay = 86400000;
|
||||||
|
|
||||||
let days = Math.floor(durationInMilliseconds / millisecondsPerDay);
|
const days = Math.floor(durationInMilliseconds / millisecondsPerDay);
|
||||||
let hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
|
const hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
|
||||||
let minutes = Math.floor((durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute);
|
const minutes = Math.floor(
|
||||||
let seconds = Math.floor((durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond);
|
(durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute
|
||||||
let millis = durationInMilliseconds % millisecondsPerSecond;
|
);
|
||||||
|
const seconds = Math.floor(
|
||||||
|
(durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond
|
||||||
|
);
|
||||||
|
const millis = durationInMilliseconds % millisecondsPerSecond;
|
||||||
|
|
||||||
let durationString = '';
|
let durationString = '';
|
||||||
|
|
||||||
@@ -131,32 +141,10 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
|||||||
|
|
||||||
return durationString.trim();
|
return durationString.trim();
|
||||||
}
|
}
|
||||||
// export function debounceAsyncFunction<T extends any[], R>(
|
|
||||||
// func: (...args: T) => Promise<R>
|
export function debounceAsyncFunction<T extends (...args: never[]) => Promise<unknown>>(
|
||||||
// ): (...args: T) => Promise<R> {
|
asyncFn: T
|
||||||
// let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
): T {
|
||||||
// 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 {
|
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let latestArgs: Parameters<T> | null = null;
|
let latestArgs: Parameters<T> | null = null;
|
||||||
let resolveNext: (() => void) | null = null;
|
let resolveNext: (() => void) | null = null;
|
||||||
@@ -177,7 +165,7 @@ export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>
|
|||||||
try {
|
try {
|
||||||
// Execute with the latest arguments
|
// Execute with the latest arguments
|
||||||
const result = await asyncFn(...latestArgs!);
|
const result = await asyncFn(...latestArgs!);
|
||||||
return result;
|
return result as ReturnType<T>;
|
||||||
} finally {
|
} finally {
|
||||||
// Allow the next execution
|
// Allow the next execution
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
@@ -190,48 +178,18 @@ export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
|
export function withArgsChangeOnly<T extends unknown[], R>(
|
||||||
// let currentPromise: Promise<R> | null = null;
|
func: (...args: T) => R
|
||||||
// let nextArgs: T | null = null;
|
): (...args: T) => R {
|
||||||
// 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 {
|
|
||||||
let lastArgs: T | undefined = undefined;
|
let lastArgs: T | undefined = undefined;
|
||||||
let lastResult: R;
|
let lastResult: R;
|
||||||
|
|
||||||
return (...args: T): R => {
|
return (...args: T): R => {
|
||||||
// Check if arguments are the same as last call
|
// 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
|
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
|
return lastResult; // Return new result
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { browser } from "$app/environment";
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export class LocalStore<T> {
|
export class LocalStore<T> {
|
||||||
value = $state<T>() as T;
|
value = $state<T>() as T;
|
||||||
key = "";
|
key = '';
|
||||||
|
|
||||||
constructor(key: string, value: T) {
|
constructor(key: string, value: T) {
|
||||||
this.key = key;
|
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> {
|
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 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>) {
|
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||||
|
|
||||||
let store: Writable<T>;
|
let store: Writable<T>;
|
||||||
|
|
||||||
if (HAS_LOCALSTORAGE) {
|
if (HAS_LOCALSTORAGE) {
|
||||||
@@ -36,18 +35,15 @@ function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
|||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
set: store.set,
|
set: store.set,
|
||||||
update: store.update
|
update: store.update
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
||||||
|
|
||||||
if (storeIds.has(key)) return storeIds.get(key) as 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);
|
storeIds.set(key, store);
|
||||||
|
|
||||||
return store
|
return store;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default <T extends unknown[]>(
|
export default <T extends unknown[]>(
|
||||||
callback: (...args: T) => void,
|
callback: (...args: T) => void,
|
||||||
delay: number,
|
delay: number
|
||||||
) => {
|
) => {
|
||||||
let isWaiting = false;
|
let isWaiting = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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="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="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"/>
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 496 B After Width: | Height: | Size: 509 B |
@@ -8,6 +8,7 @@ export async function getWasm(id: `${string}/${string}/${string}`) {
|
|||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(`Failed to read node: ${id}`, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,12 +21,11 @@ export async function getNodeWasm(id: `${string}/${string}/${string}`) {
|
|||||||
const wasmBytes = await getWasm(id);
|
const wasmBytes = await getWasm(id);
|
||||||
if (!wasmBytes) return null;
|
if (!wasmBytes) return null;
|
||||||
|
|
||||||
const wrapper = createWasmWrapper(
|
try {
|
||||||
wasmBytes.buffer,
|
return createWasmWrapper(wasmBytes.buffer);
|
||||||
new WebAssembly.Memory({ initial: 1024, maximum: 8192 })
|
} catch (error) {
|
||||||
);
|
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
||||||
|
}
|
||||||
return wrapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNode(id: `${string}/${string}/${string}`) {
|
export async function getNode(id: `${string}/${string}/${string}`) {
|
||||||
|
|||||||
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';
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { AsyncCache } from '@nodarium/types';
|
import type { AsyncCache } from '@nodarium/types';
|
||||||
import { openDB, type IDBPDatabase } from 'idb';
|
import { type IDBPDatabase, openDB } from 'idb';
|
||||||
|
|
||||||
export class IndexDBCache implements AsyncCache<unknown> {
|
export class IndexDBCache implements AsyncCache<unknown> {
|
||||||
|
|
||||||
size: number = 100;
|
size: number = 100;
|
||||||
|
|
||||||
db: Promise<IDBPDatabase<unknown>>;
|
db: Promise<IDBPDatabase<unknown>>;
|
||||||
@@ -12,7 +11,7 @@ export class IndexDBCache implements AsyncCache<unknown> {
|
|||||||
this.db = openDB<unknown>('cache/' + id, 1, {
|
this.db = openDB<unknown>('cache/' + id, 1, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
db.createObjectStore('keyval');
|
db.createObjectStore('keyval');
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +32,16 @@ export class IndexDBCache implements AsyncCache<unknown> {
|
|||||||
if (res instanceof ArrayBuffer) {
|
if (res instanceof ArrayBuffer) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getString(key: string) {
|
async getString(key: string) {
|
||||||
const res = await this.get(key);
|
const res = await this.get(key);
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
if (typeof res === "string") {
|
if (typeof res === 'string') {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: unknown) {
|
async set(key: string, value: unknown) {
|
||||||
@@ -54,5 +53,4 @@ export class IndexDBCache implements AsyncCache<unknown> {
|
|||||||
clear() {
|
clear() {
|
||||||
this.db.then(db => db.clear('keyval'));
|
this.db.then(db => db.clear('keyval'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
type AsyncCache,
|
type AsyncCache,
|
||||||
type NodeDefinition,
|
type NodeDefinition,
|
||||||
NodeDefinitionSchema,
|
NodeDefinitionSchema,
|
||||||
type NodeId,
|
|
||||||
type NodeRegistry
|
type NodeRegistry
|
||||||
} from '@nodarium/types';
|
} from '@nodarium/types';
|
||||||
import { createLogger, createWasmWrapper } from '@nodarium/utils';
|
import { createLogger, createWasmWrapper } from '@nodarium/utils';
|
||||||
@@ -13,12 +12,11 @@ log.mute();
|
|||||||
export class RemoteNodeRegistry implements NodeRegistry {
|
export class RemoteNodeRegistry implements NodeRegistry {
|
||||||
status: 'loading' | 'ready' | 'error' = 'loading';
|
status: 'loading' | 'ready' | 'error' = 'loading';
|
||||||
private nodes: Map<string, NodeDefinition> = new Map();
|
private nodes: Map<string, NodeDefinition> = new Map();
|
||||||
private memory = new WebAssembly.Memory({ initial: 1024, maximum: 8192 });
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private url: string,
|
private url: string,
|
||||||
public cache?: AsyncCache<ArrayBuffer | string>
|
public cache?: AsyncCache<ArrayBuffer | string>
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async fetchJson(url: string, skipCache = false) {
|
async fetchJson(url: string, skipCache = false) {
|
||||||
const finalUrl = `${this.url}/${url}`;
|
const finalUrl = `${this.url}/${url}`;
|
||||||
@@ -107,7 +105,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
const wasmBuffer = await this.fetchNodeWasm(id);
|
const wasmBuffer = await this.fetchNodeWasm(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.register(wasmBuffer);
|
return await this.register(id, wasmBuffer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed to register: ', id);
|
console.log('Failed to register: ', id);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -127,23 +125,33 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(wasmBuffer: ArrayBuffer) {
|
async register(id: string, wasmBuffer: ArrayBuffer) {
|
||||||
const wrapper = createWasmWrapper(
|
let wrapper: ReturnType<typeof createWasmWrapper> = null!;
|
||||||
wasmBuffer,
|
try {
|
||||||
this.memory
|
wrapper = createWasmWrapper(wasmBuffer);
|
||||||
);
|
} catch (error) {
|
||||||
|
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
|
const rawDefinition = wrapper.get_definition();
|
||||||
|
const definition = NodeDefinitionSchema.safeParse(rawDefinition);
|
||||||
|
|
||||||
if (definition.error) {
|
if (definition.error) {
|
||||||
throw definition.error;
|
throw new Error(
|
||||||
|
'Failed to parse node definition from node:+\n' + JSON.stringify(rawDefinition, null, 2)
|
||||||
|
+ '\n'
|
||||||
|
+ definition.error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
this.cache.set(definition.data.id, wasmBuffer);
|
this.cache.set(definition.data.id, wasmBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let node = { ...definition.data, execute: wrapper.execute };
|
const node = {
|
||||||
|
...definition.data,
|
||||||
|
execute: wrapper.execute
|
||||||
|
};
|
||||||
|
|
||||||
this.nodes.set(definition.data.id, node);
|
this.nodes.set(definition.data.id, node);
|
||||||
|
|
||||||
@@ -155,13 +163,6 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllNodes() {
|
getAllNodes() {
|
||||||
const allNodes = [...this.nodes.values()];
|
return [...this.nodes.values()];
|
||||||
log.info('getting all nodes', allNodes);
|
|
||||||
return allNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async overwriteNode(nodeId: NodeId, node: NodeDefinition) {
|
|
||||||
log.info('Overwritten node', { nodeId, node });
|
|
||||||
this.nodes.set(nodeId, node);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Select } from "@nodarium/ui";
|
import { InputSelect } from '@nodarium/ui';
|
||||||
|
|
||||||
let activeStore = $state(0);
|
let activeStore = $state(0);
|
||||||
let { activeId }: { activeId: string } = $props();
|
let { activeId }: { activeId: string } = $props();
|
||||||
const [activeUser, activeCollection, activeNode] = $derived(
|
const [activeUser, activeCollection, activeNode] = $derived(
|
||||||
activeId.split(`/`),
|
activeId.split(`/`)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
{#if activeUser}
|
{#if activeUser}
|
||||||
<Select id="root" options={["root"]} bind:value={activeStore}></Select>
|
<InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
|
||||||
{#if activeCollection}
|
{#if activeCollection}
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<span>{activeUser}</span>
|
<span>{activeUser}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Select id="root" options={["root"]} bind:value={activeStore}></Select>
|
<InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
gap: 0.8em;
|
gap: 0.8em;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: solid thin var(--outline);
|
border-bottom: solid thin var(--color-outline);
|
||||||
}
|
}
|
||||||
.breadcrumbs > button {
|
.breadcrumbs > button {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,39 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
|
import NodeHtml from '$lib/graph-interface/node/NodeHTML.svelte';
|
||||||
import type { NodeDefinition, NodeId, NodeInstance } from "@nodarium/types";
|
import type { NodeDefinition, NodeId, NodeInstance } from '@nodarium/types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const { node }: { node: NodeDefinition } = $props();
|
const { node }: { node: NodeDefinition } = $props();
|
||||||
|
|
||||||
let dragging = $state(false);
|
let dragging = $state(false);
|
||||||
|
|
||||||
let nodeData = $state<NodeInstance>({
|
let nodeData = $state<NodeInstance>(null!);
|
||||||
id: 0,
|
|
||||||
type: node.id as unknown as NodeId,
|
|
||||||
position: [0, 0] as [number, number],
|
|
||||||
props: {},
|
|
||||||
state: {
|
|
||||||
type: node,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleDragStart(e: DragEvent) {
|
function handleDragStart(e: DragEvent) {
|
||||||
dragging = true;
|
dragging = true;
|
||||||
const box = (e?.target as HTMLElement)?.getBoundingClientRect();
|
const box = (e?.target as HTMLElement)?.getBoundingClientRect();
|
||||||
if (e.dataTransfer === null) return;
|
if (e.dataTransfer === null) return;
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData("data/node-id", node.id.toString());
|
e.dataTransfer.setData('data/node-id', node.id.toString());
|
||||||
if (nodeData.props) {
|
if (nodeData?.props) {
|
||||||
e.dataTransfer.setData("data/node-props", JSON.stringify(nodeData.props));
|
e.dataTransfer.setData('data/node-props', JSON.stringify(nodeData.props));
|
||||||
}
|
}
|
||||||
e.dataTransfer.setData(
|
e.dataTransfer.setData(
|
||||||
"data/node-offset-x",
|
'data/node-offset-x',
|
||||||
Math.round(box.left - e.clientX).toString(),
|
Math.round(box.left - e.clientX).toString()
|
||||||
);
|
);
|
||||||
e.dataTransfer.setData(
|
e.dataTransfer.setData(
|
||||||
"data/node-offset-y",
|
'data/node-offset-y',
|
||||||
Math.round(box.top - e.clientY).toString(),
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="node-wrapper" class:dragging>
|
<div class="node-wrapper" class:dragging>
|
||||||
@@ -46,7 +51,9 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
ondragstart={handleDragStart}
|
ondragstart={handleDragStart}
|
||||||
>
|
>
|
||||||
<NodeHtml bind:node={nodeData} inView={true} position={"relative"} z={5} />
|
{#if nodeData}
|
||||||
|
<NodeHtml bind:node={nodeData} inView={true} position="relative" z={5} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dragging {
|
.dragging {
|
||||||
border: dashed 2px var(--outline);
|
border: dashed 2px var(--color-outline);
|
||||||
}
|
}
|
||||||
.node-wrapper > div {
|
.node-wrapper > div {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BreadCrumbs from "./BreadCrumbs.svelte";
|
import type { RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||||
import DraggableNode from "./DraggableNode.svelte";
|
import BreadCrumbs from './BreadCrumbs.svelte';
|
||||||
import type { RemoteNodeRegistry } from "@nodarium/registry";
|
import DraggableNode from './DraggableNode.svelte';
|
||||||
|
|
||||||
const { registry }: { registry: RemoteNodeRegistry } = $props();
|
const { registry }: { registry: RemoteNodeRegistry } = $props();
|
||||||
|
|
||||||
let activeId = $state("max/plantarium");
|
let activeId = $state('max/plantarium');
|
||||||
let showBreadCrumbs = false;
|
let showBreadCrumbs = false;
|
||||||
|
|
||||||
const [activeUser, activeCollection, activeNode] = $derived(
|
const [activeUser, activeCollection, activeNode] = $derived(
|
||||||
activeId.split(`/`),
|
activeId.split(`/`)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -22,12 +22,14 @@
|
|||||||
{#await registry.fetchUsers()}
|
{#await registry.fetchUsers()}
|
||||||
<div>Loading Users...</div>
|
<div>Loading Users...</div>
|
||||||
{:then users}
|
{:then users}
|
||||||
{#each users as user}
|
{#each users as user (user.id)}
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeId = user.id;
|
activeId = user.id;
|
||||||
}}>{user.id}</button
|
}}
|
||||||
>
|
>
|
||||||
|
{user.id}
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<div>{error.message}</div>
|
<div>{error.message}</div>
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
{#await registry.fetchUser(activeUser)}
|
{#await registry.fetchUser(activeUser)}
|
||||||
<div>Loading User...</div>
|
<div>Loading User...</div>
|
||||||
{:then user}
|
{:then user}
|
||||||
{#each user.collections as collection}
|
{#each user.collections as collection (collection)}
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeId = collection.id;
|
activeId = collection.id;
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
|
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
|
||||||
<div>Loading Collection...</div>
|
<div>Loading Collection...</div>
|
||||||
{:then collection}
|
{:then collection}
|
||||||
{#each collection.nodes as node}
|
{#each collection.nodes as node (node.id)}
|
||||||
{#await registry.fetchNodeDefinition(node.id)}
|
{#await registry.fetchNodeDefinition(node.id)}
|
||||||
<div>Loading Node... {node.id}</div>
|
<div>Loading Node... {node.id}</div>
|
||||||
{:then node}
|
{:then node}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@
|
|||||||
|
|
||||||
const total = $derived(values.reduce((acc, v) => acc + v, 0));
|
const total = $derived(values.reduce((acc, v) => acc + v, 0));
|
||||||
|
|
||||||
let colors = ["red", "green", "blue"];
|
let colors = ['red', 'green', 'blue'];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="bars">
|
<div class="bars">
|
||||||
{#each values as value, i}
|
{#each values as value, i (value)}
|
||||||
<div
|
<div
|
||||||
class="bar bg-{colors[i]}-400"
|
class="bar bg-{colors[i]}-400"
|
||||||
style="width: {(value / total) * 100}%;"
|
style:width={(value / total) * 100 + '%'}
|
||||||
>
|
>
|
||||||
{Math.round(value)}ms
|
{Math.round(value)}ms
|
||||||
</div>
|
</div>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="labels mt-2">
|
<div class="labels mt-2">
|
||||||
{#each values as _label, i}
|
{#each values as _label, i (_label)}
|
||||||
<div class="text-{colors[i]}-400">{labels[i]}</div>
|
<div class="text-{colors[i]}-400">{labels[i]}</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,17 +9,17 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
points,
|
points,
|
||||||
type = "ms",
|
type = 'ms',
|
||||||
title = "Performance",
|
title = 'Performance',
|
||||||
max,
|
max,
|
||||||
min,
|
min
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let internalMax = $derived(max ?? Math.max(...points));
|
let internalMax = $derived(max ?? Math.max(...points));
|
||||||
let internalMin = $derived(min ?? Math.min(...points))!;
|
let internalMin = $derived(min ?? Math.min(...points))!;
|
||||||
|
|
||||||
const maxText = $derived.by(() => {
|
const maxText = $derived.by(() => {
|
||||||
if (type === "%") {
|
if (type === '%') {
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +40,10 @@
|
|||||||
points
|
points
|
||||||
.map((point, i) => {
|
.map((point, i) => {
|
||||||
const x = (i / (points.length - 1)) * 100;
|
const x = (i / (points.length - 1)) * 100;
|
||||||
const y =
|
const y = 100 - ((point - internalMin) / (internalMax - internalMin)) * 100;
|
||||||
100 - ((point - internalMin) / (internalMax - internalMin)) * 100;
|
|
||||||
return `${x},${y}`;
|
return `${x},${y}`;
|
||||||
})
|
})
|
||||||
.join(" "),
|
.join(' ')
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: solid thin var(--outline);
|
border-bottom: solid thin var(--color-outline);
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
@@ -89,13 +88,13 @@
|
|||||||
svg {
|
svg {
|
||||||
height: 124px;
|
height: 124px;
|
||||||
margin: 24px 0px;
|
margin: 24px 0px;
|
||||||
border-top: solid thin var(--outline);
|
border-top: solid thin var(--color-outline);
|
||||||
border-bottom: solid thin var(--outline);
|
border-bottom: solid thin var(--color-outline);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
polyline {
|
polyline {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: var(--layer-3);
|
stroke: var(--color-layer-3);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Monitor from "./Monitor.svelte";
|
import { humanizeNumber } from '$lib/helpers';
|
||||||
import { humanizeNumber } from "$lib/helpers";
|
import { InputCheckbox } from '@nodarium/ui';
|
||||||
import { Checkbox } from "@nodarium/ui";
|
import type { PerformanceData } from '@nodarium/utils';
|
||||||
import type { PerformanceData } from "@nodarium/utils";
|
import BarSplit from './BarSplit.svelte';
|
||||||
import BarSplit from "./BarSplit.svelte";
|
import Monitor from './Monitor.svelte';
|
||||||
|
|
||||||
const { data }: { data: PerformanceData } = $props();
|
const { data }: { data: PerformanceData } = $props();
|
||||||
|
|
||||||
let activeType = $state("total");
|
let activeType = $state('total');
|
||||||
let showAverage = $state(true);
|
let showAverage = $state(true);
|
||||||
|
|
||||||
function round(v: number) {
|
function round(v: number) {
|
||||||
@@ -21,21 +21,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTitle(t: string) {
|
function getTitle(t: string) {
|
||||||
if (t.includes("/")) {
|
if (t.includes('/')) {
|
||||||
return `Node ${t.split("/").slice(-1).join("/")}`;
|
return `Node ${t.split('/').slice(-1).join('/')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return t
|
return t
|
||||||
.split("-")
|
.split('-')
|
||||||
.map((v) => v[0].toUpperCase() + v.slice(1))
|
.map((v) => v[0].toUpperCase() + v.slice(1))
|
||||||
.join(" ");
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewerKeys = [
|
const viewerKeys = [
|
||||||
"total-vertices",
|
'total-vertices',
|
||||||
"total-faces",
|
'total-faces',
|
||||||
"update-geometries",
|
'update-geometries',
|
||||||
"split-result",
|
'split-result'
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Small helpers that query `data` directly ---
|
// --- Small helpers that query `data` directly ---
|
||||||
@@ -64,21 +64,19 @@
|
|||||||
const lasts = $derived.by(() => data.at(-1) || {});
|
const lasts = $derived.by(() => data.at(-1) || {});
|
||||||
|
|
||||||
const totalPerformance = $derived.by(() => {
|
const totalPerformance = $derived.by(() => {
|
||||||
const onlyLast =
|
const onlyLast = getLast('runtime')
|
||||||
getLast("runtime") +
|
+ getLast('update-geometries')
|
||||||
getLast("update-geometries") +
|
+ getLast('worker-transfer');
|
||||||
getLast("worker-transfer");
|
const average = getAverage('runtime')
|
||||||
const average =
|
+ getAverage('update-geometries')
|
||||||
getAverage("runtime") +
|
+ getAverage('worker-transfer');
|
||||||
getAverage("update-geometries") +
|
|
||||||
getAverage("worker-transfer");
|
|
||||||
return { onlyLast, average };
|
return { onlyLast, average };
|
||||||
});
|
});
|
||||||
|
|
||||||
const cacheRatio = $derived.by(() => {
|
const cacheRatio = $derived.by(() => {
|
||||||
return {
|
return {
|
||||||
onlyLast: Math.floor(getLast("cache-hit") * 100),
|
onlyLast: Math.floor(getLast('cache-hit') * 100),
|
||||||
average: Math.floor(getAverage("cache-hit") * 100),
|
average: Math.floor(getAverage('cache-hit') * 100)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,10 +85,10 @@
|
|||||||
return Object.entries(source)
|
return Object.entries(source)
|
||||||
.filter(
|
.filter(
|
||||||
([key]) =>
|
([key]) =>
|
||||||
!key.startsWith("node/") &&
|
!key.startsWith('node/')
|
||||||
key !== "total" &&
|
&& key !== 'total'
|
||||||
!key.includes("cache") &&
|
&& !key.includes('cache')
|
||||||
!viewerKeys.includes(key),
|
&& !viewerKeys.includes(key)
|
||||||
)
|
)
|
||||||
.sort((a, b) => b[1] - a[1]);
|
.sort((a, b) => b[1] - a[1]);
|
||||||
});
|
});
|
||||||
@@ -98,7 +96,7 @@
|
|||||||
const nodePerformanceData = $derived.by(() => {
|
const nodePerformanceData = $derived.by(() => {
|
||||||
const source = showAverage ? averages : lasts;
|
const source = showAverage ? averages : lasts;
|
||||||
return Object.entries(source)
|
return Object.entries(source)
|
||||||
.filter(([key]) => key.startsWith("node/"))
|
.filter(([key]) => key.startsWith('node/'))
|
||||||
.sort((a, b) => b[1] - a[1]);
|
.sort((a, b) => b[1] - a[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,9 +105,9 @@
|
|||||||
return Object.entries(source)
|
return Object.entries(source)
|
||||||
.filter(
|
.filter(
|
||||||
([key]) =>
|
([key]) =>
|
||||||
key !== "total-vertices" &&
|
key !== 'total-vertices'
|
||||||
key !== "total-faces" &&
|
&& key !== 'total-faces'
|
||||||
viewerKeys.includes(key),
|
&& viewerKeys.includes(key)
|
||||||
)
|
)
|
||||||
.sort((a, b) => b[1] - a[1]);
|
.sort((a, b) => b[1] - a[1]);
|
||||||
});
|
});
|
||||||
@@ -117,15 +115,15 @@
|
|||||||
const splitValues = $derived.by(() => {
|
const splitValues = $derived.by(() => {
|
||||||
if (showAverage) {
|
if (showAverage) {
|
||||||
return [
|
return [
|
||||||
getAverage("worker-transfer"),
|
getAverage('worker-transfer'),
|
||||||
getAverage("runtime"),
|
getAverage('runtime'),
|
||||||
getAverage("update-geometries"),
|
getAverage('update-geometries')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
getLast("worker-transfer"),
|
getLast('worker-transfer'),
|
||||||
getLast("runtime"),
|
getLast('runtime'),
|
||||||
getLast("update-geometries"),
|
getLast('update-geometries')
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,24 +131,24 @@
|
|||||||
if (showAverage) {
|
if (showAverage) {
|
||||||
return data.map((run) => {
|
return data.map((run) => {
|
||||||
return (
|
return (
|
||||||
(run["runtime"]?.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['update-geometries']?.reduce((acc, v) => acc + v, 0) || 0)
|
||||||
(run["worker-transfer"]?.reduce((acc, v) => acc + v, 0) || 0)
|
+ (run['worker-transfer']?.reduce((acc, v) => acc + v, 0) || 0)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.map((run) => {
|
return data.map((run) => {
|
||||||
return (
|
return (
|
||||||
(run["runtime"]?.[0] || 0) +
|
(run['runtime']?.[0] || 0)
|
||||||
(run["update-geometries"]?.[0] || 0) +
|
+ (run['update-geometries']?.[0] || 0)
|
||||||
(run["worker-transfer"]?.[0] || 0)
|
+ (run['worker-transfer']?.[0] || 0)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function constructPoints(key: string) {
|
function constructPoints(key: string) {
|
||||||
if (key === "total") {
|
if (key === 'total') {
|
||||||
return totalPoints;
|
return totalPoints;
|
||||||
}
|
}
|
||||||
return data.map((run) => {
|
return data.map((run) => {
|
||||||
@@ -166,21 +164,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const computedTotalDisplay = $derived.by(() =>
|
const computedTotalDisplay = $derived.by(() =>
|
||||||
round(showAverage ? totalPerformance.average : totalPerformance.onlyLast),
|
round(showAverage ? totalPerformance.average : totalPerformance.onlyLast)
|
||||||
);
|
);
|
||||||
|
|
||||||
const computedFps = $derived.by(() =>
|
const computedFps = $derived.by(() =>
|
||||||
Math.floor(
|
Math.floor(
|
||||||
1000 /
|
1000
|
||||||
(showAverage
|
/ (showAverage
|
||||||
? totalPerformance.average || 1
|
? totalPerformance.average || 1
|
||||||
: totalPerformance.onlyLast || 1),
|
: totalPerformance.onlyLast || 1)
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data.length !== 0}
|
{#if data.length !== 0}
|
||||||
{#if activeType === "cache-hit"}
|
{#if activeType === 'cache-hit'}
|
||||||
<Monitor
|
<Monitor
|
||||||
title="Cache Hits"
|
title="Cache Hits"
|
||||||
points={constructPoints(activeType)}
|
points={constructPoints(activeType)}
|
||||||
@@ -197,12 +195,12 @@
|
|||||||
|
|
||||||
<div class="p-4 performance-tabler">
|
<div class="p-4 performance-tabler">
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
<label for="show-total">Show Average</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BarSplit
|
<BarSplit
|
||||||
labels={["worker-transfer", "runtime", "update-geometries"]}
|
labels={['worker-transfer', 'runtime', 'update-geometries']}
|
||||||
values={splitValues}
|
values={splitValues}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -215,14 +213,14 @@
|
|||||||
{computedTotalDisplay}<span>ms</span>
|
{computedTotalDisplay}<span>ms</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class:active={activeType === "total"}
|
class:active={activeType === 'total'}
|
||||||
onclick={() => (activeType = "total")}
|
onclick={() => (activeType = 'total')}
|
||||||
>
|
>
|
||||||
total<span>({computedFps}fps)</span>
|
total<span>({computedFps}fps)</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{#each performanceData as [key, value]}
|
{#each performanceData as [key, value] (key)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{round(value)}<span>ms</span></td>
|
<td>{round(value)}<span>ms</span></td>
|
||||||
<td
|
<td
|
||||||
@@ -246,27 +244,23 @@
|
|||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span>%</span></td>
|
||||||
<td
|
<td
|
||||||
>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span
|
class:active={activeType === 'cache-hit'}
|
||||||
>%</span
|
onclick={() => (activeType = 'cache-hit')}
|
||||||
></td
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
class:active={activeType === "cache-hit"}
|
|
||||||
onclick={() => (activeType = "cache-hit")}
|
|
||||||
>
|
>
|
||||||
cache hits
|
cache hits
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{#each nodePerformanceData as [key, value]}
|
{#each nodePerformanceData as [key, value] (key)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{round(value)}<span>ms</span></td>
|
<td>{round(value)}<span>ms</span></td>
|
||||||
<td
|
<td
|
||||||
class:active={activeType === key}
|
class:active={activeType === key}
|
||||||
onclick={() => (activeType = key)}
|
onclick={() => (activeType = key)}
|
||||||
>
|
>
|
||||||
{key.split("/").slice(-1).join("/")}
|
{key.split('/').slice(-1).join('/')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -278,22 +272,22 @@
|
|||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{humanizeNumber(getLast("total-vertices"))}</td>
|
<td>{humanizeNumber(getLast('total-vertices'))}</td>
|
||||||
<td>Vertices</td>
|
<td>Vertices</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{humanizeNumber(getLast("total-faces"))}</td>
|
<td>{humanizeNumber(getLast('total-faces'))}</td>
|
||||||
<td>Faces</td>
|
<td>Faces</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{#each viewerPerformanceData as [key, value]}
|
{#each viewerPerformanceData as [key, value] (key)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{round(value)}<span>ms</span></td>
|
<td>{round(value)}<span>ms</span></td>
|
||||||
<td
|
<td
|
||||||
class:active={activeType === key}
|
class:active={activeType === key}
|
||||||
onclick={() => (activeType = key)}
|
onclick={() => (activeType = key)}
|
||||||
>
|
>
|
||||||
{key.split("/").slice(-1).join("/")}
|
{key.split('/').slice(-1).join('/')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
const y = 100 - ((point - min) / (max - min)) * 100;
|
const y = 100 - ((point - min) / (max - min)) * 100;
|
||||||
return `${x},${y}`;
|
return `${x},${y}`;
|
||||||
})
|
})
|
||||||
.join(" ");
|
.join(' ');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
polyline {
|
polyline {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: var(--layer-3);
|
stroke: var(--color-layer-3);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { humanizeDuration, humanizeNumber } from "$lib/helpers";
|
import { humanizeDuration, humanizeNumber } from '$lib/helpers';
|
||||||
import { localState } from "$lib/helpers/localState.svelte";
|
import { localState } from '$lib/helpers/localState.svelte';
|
||||||
import SmallGraph from "./SmallGraph.svelte";
|
import type { PerformanceData, PerformanceStore } from '@nodarium/utils';
|
||||||
import type { PerformanceData, PerformanceStore } from "@nodarium/utils";
|
import SmallGraph from './SmallGraph.svelte';
|
||||||
|
|
||||||
const { store, fps }: { store: PerformanceStore; fps: number[] } = $props();
|
const { store, fps }: { store: PerformanceStore; fps: number[] } = $props();
|
||||||
|
|
||||||
const open = localState("node.performance.small.open", {
|
const open = localState('node.performance.small.open', {
|
||||||
runtime: false,
|
runtime: false,
|
||||||
fps: false,
|
fps: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const vertices = $derived($store?.at(-1)?.["total-vertices"]?.[0] || 0);
|
const vertices = $derived($store?.at(-1)?.['total-vertices']?.[0] || 0);
|
||||||
const faces = $derived($store?.at(-1)?.["total-faces"]?.[0] || 0);
|
const faces = $derived($store?.at(-1)?.['total-faces']?.[0] || 0);
|
||||||
const runtime = $derived($store?.at(-1)?.["runtime"]?.[0] || 0);
|
const runtime = $derived($store?.at(-1)?.['runtime']?.[0] || 0);
|
||||||
|
|
||||||
function getPoints(data: PerformanceData, key: string) {
|
function getPoints(data: PerformanceData, key: string) {
|
||||||
return data?.map((run) => run[key]?.[0] || 0) || [];
|
return data?.map((run) => run[key]?.[0] || 0) || [];
|
||||||
@@ -24,25 +24,25 @@
|
|||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
style="cursor:pointer;"
|
style="cursor: pointer"
|
||||||
onclick={() => (open.value.runtime = !open.value.runtime)}
|
onclick={() => (open.value.runtime = !open.value.runtime)}
|
||||||
>
|
>
|
||||||
<td>{open.value.runtime ? "-" : "+"} runtime </td>
|
<td>{open.value.runtime ? '-' : '+'} runtime</td>
|
||||||
<td>{humanizeDuration(runtime || 1000)}</td>
|
<td>{humanizeDuration(runtime || 1000)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{#if open.value.runtime}
|
{#if open.value.runtime}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<SmallGraph points={getPoints($store, "runtime")} />
|
<SmallGraph points={getPoints($store, 'runtime')} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<tr
|
<tr
|
||||||
style="cursor:pointer;"
|
style="cursor: pointer"
|
||||||
onclick={() => (open.value.fps = !open.value.fps)}
|
onclick={() => (open.value.fps = !open.value.fps)}
|
||||||
>
|
>
|
||||||
<td>{open.value.fps ? "-" : "+"} fps </td>
|
<td>{open.value.fps ? '-' : '+'} fps</td>
|
||||||
<td>
|
<td>
|
||||||
{Math.floor(fps[fps.length - 1])}fps
|
{Math.floor(fps[fps.length - 1])}fps
|
||||||
</td>
|
</td>
|
||||||
@@ -56,12 +56,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>vertices </td>
|
<td>vertices</td>
|
||||||
<td>{humanizeNumber(vertices || 0)}</td>
|
<td>{humanizeNumber(vertices || 0)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>faces </td>
|
<td>faces</td>
|
||||||
<td>{humanizeNumber(faces || 0)}</td>
|
<td>{humanizeNumber(faces || 0)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -74,14 +74,14 @@
|
|||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: var(--layer-0);
|
background: var(--color-layer-0);
|
||||||
border: solid thin var(--outline);
|
border: solid thin var(--color-outline);
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
border: solid thin var(--outline);
|
border: solid thin var(--color-outline);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as PerformanceViewer } from "./PerformanceViewer.svelte";
|
export { default as PerformanceViewer } from './PerformanceViewer.svelte';
|
||||||
|
|||||||
@@ -1,43 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Graph } from "$lib/types";
|
import { defaultPlant, lottaFaces, plant, simple } from '$lib/graph-templates';
|
||||||
import { defaultPlant, plant, lottaFaces } from "$lib/graph-templates";
|
import type { Graph } from '$lib/types';
|
||||||
import type { ProjectManager } from "./project-manager.svelte";
|
import { InputSelect } from '@nodarium/ui';
|
||||||
|
import type { ProjectManager } from './project-manager.svelte';
|
||||||
|
|
||||||
const { projectManager } = $props<{ projectManager: ProjectManager }>();
|
const { projectManager } = $props<{ projectManager: ProjectManager }>();
|
||||||
|
|
||||||
let showNewProject = $state(false);
|
let showNewProject = $state(false);
|
||||||
let newProjectName = $state("");
|
let newProjectName = $state('');
|
||||||
let selectedTemplate = $state("defaultPlant");
|
|
||||||
|
|
||||||
const templates = [
|
const templates = [
|
||||||
{
|
{
|
||||||
name: "Default Plant",
|
name: 'Default Plant',
|
||||||
value: "defaultPlant",
|
value: 'defaultPlant',
|
||||||
graph: defaultPlant as unknown as Graph,
|
graph: defaultPlant as unknown as Graph
|
||||||
},
|
},
|
||||||
{ name: "Plant", value: "plant", graph: plant as unknown as Graph },
|
{ name: 'Plant', value: 'plant', graph: plant as unknown as Graph },
|
||||||
|
{ name: 'Simple', value: 'simple', graph: simple as unknown as Graph },
|
||||||
{
|
{
|
||||||
name: "Lotta Faces",
|
name: 'Lotta Faces',
|
||||||
value: "lottaFaces",
|
value: 'lottaFaces',
|
||||||
graph: lottaFaces as unknown as Graph,
|
graph: lottaFaces as unknown as Graph
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
let selectedTemplateIndex = $state(0);
|
||||||
|
|
||||||
function handleCreate() {
|
function handleCreate() {
|
||||||
const template =
|
const template = templates[selectedTemplateIndex] || templates[0];
|
||||||
templates.find((t) => t.value === selectedTemplate) || templates[0];
|
|
||||||
projectManager.handleCreateProject(template.graph, newProjectName);
|
projectManager.handleCreateProject(template.graph, newProjectName);
|
||||||
newProjectName = "";
|
newProjectName = '';
|
||||||
showNewProject = false;
|
showNewProject = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header class="flex justify-between px-4 h-[70px] border-b-1 border-outline items-center bg-layer-2">
|
||||||
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center"
|
|
||||||
>
|
|
||||||
<h3>Project</h3>
|
<h3>Project</h3>
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1 bg-[var(--layer-0)] rounded"
|
class="px-3 py-1 bg-layer-1 rounded"
|
||||||
onclick={() => (showNewProject = !showNewProject)}
|
onclick={() => (showNewProject = !showNewProject)}
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
@@ -45,24 +44,17 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if showNewProject}
|
{#if showNewProject}
|
||||||
<div class="flex flex-col px-4 py-3 border-b-1 border-[var(--outline)] gap-2">
|
<div class="flex flex-col px-4 py-3.5 mt-[1px] border-b-1 border-outline gap-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newProjectName}
|
bind:value={newProjectName}
|
||||||
placeholder="Project name"
|
placeholder="Project name"
|
||||||
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
|
class="w-full px-2 py-2 bg-layer-2 rounded"
|
||||||
onkeydown={(e) => e.key === "Enter" && handleCreate()}
|
onkeydown={(e) => e.key === 'Enter' && handleCreate()}
|
||||||
/>
|
/>
|
||||||
<select
|
<InputSelect options={templates.map(t => t.name)} bind:value={selectedTemplateIndex} />
|
||||||
bind:value={selectedTemplate}
|
|
||||||
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
|
|
||||||
>
|
|
||||||
{#each templates as template}
|
|
||||||
<option value={template.value}>{template.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<button
|
<button
|
||||||
class="cursor-pointer self-end px-3 py-1 bg-blue-600 rounded"
|
class="cursor-pointer self-end px-3 py-1 bg-selected rounded"
|
||||||
onclick={() => handleCreate()}
|
onclick={() => handleCreate()}
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
@@ -70,30 +62,34 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="p-4 text-white min-h-screen">
|
<div class="text-white min-h-screen">
|
||||||
{#if projectManager.loading}
|
{#if projectManager.loading}
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ul class="space-y-2">
|
<ul>
|
||||||
{#each projectManager.projects as project (project.id)}
|
{#each projectManager.projects as project (project.id)}
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
class="w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
|
class="
|
||||||
|
h-[70px] border-b-1 border-b-outline
|
||||||
|
flex
|
||||||
|
w-full text-left px-3 py-2 cursor-pointer {projectManager
|
||||||
.activeProjectId.value === project.id
|
.activeProjectId.value === project.id
|
||||||
? 'bg-blue-600'
|
? 'border-l-2 border-l-selected pl-2.5!'
|
||||||
: 'bg-gray-800 hover:bg-gray-700'}"
|
: ''}
|
||||||
|
"
|
||||||
onclick={() => projectManager.handleSelectProject(project.id!)}
|
onclick={() => projectManager.handleSelectProject(project.id!)}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onkeydown={(e) =>
|
onkeydown={(e) =>
|
||||||
e.key === "Enter" &&
|
e.key === 'Enter'
|
||||||
projectManager.handleSelectProject(project.id!)}
|
&& projectManager.handleSelectProject(project.id!)}
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center grow">
|
||||||
<span>{project.meta?.title || "Untitled"}</span>
|
<span>{project.meta?.title || 'Untitled'}</span>
|
||||||
<button
|
<button
|
||||||
class="text-red-400 hover:text-red-300"
|
class="text-layer-1! bg-red-500 w-7 text-xl rounded-sm cursor-pointer opacity-20 hover:opacity-80"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
projectManager.handleDeleteProject(project.id!);
|
projectManager.handleDeleteProject(project.id!);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export async function getGraph(id: number): Promise<Graph | undefined> {
|
|||||||
|
|
||||||
export async function saveGraph(graph: Graph): Promise<Graph> {
|
export async function saveGraph(graph: Graph): Promise<Graph> {
|
||||||
const db = await getDB();
|
const db = await getDB();
|
||||||
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||||
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
|
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
|
||||||
await db.put(STORE_NAME, graph);
|
await db.put(STORE_NAME, graph);
|
||||||
return graph;
|
return graph;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class ProjectManager {
|
|||||||
this.projects = await db.getGraphs();
|
this.projects = await db.getGraphs();
|
||||||
|
|
||||||
if (this.activeProjectId.value !== undefined) {
|
if (this.activeProjectId.value !== undefined) {
|
||||||
let loadedGraph = await db.getGraph(this.activeProjectId.value);
|
const loadedGraph = await db.getGraph(this.activeProjectId.value);
|
||||||
if (loadedGraph) {
|
if (loadedGraph) {
|
||||||
this.graph = loadedGraph;
|
this.graph = loadedGraph;
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ export class ProjectManager {
|
|||||||
|
|
||||||
g.id = id;
|
g.id = id;
|
||||||
if (!g.meta) g.meta = {};
|
if (!g.meta) g.meta = {};
|
||||||
if (!g.meta.title) g.meta.title = title;
|
g.meta.title = title;
|
||||||
|
|
||||||
db.saveGraph(g);
|
db.saveGraph(g);
|
||||||
this.projects = [...this.projects, g];
|
this.projects = [...this.projects, g];
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import localStore from "$lib/helpers/localStore";
|
import localStore from '$lib/helpers/localStore';
|
||||||
import { T, useTask } from "@threlte/core";
|
import { T, useTask } from '@threlte/core';
|
||||||
import { OrbitControls } from "@threlte/extras";
|
import { OrbitControls } from '@threlte/extras';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from 'svelte';
|
||||||
import { Vector3 } from "three";
|
import { Vector3 } from 'three';
|
||||||
import type { PerspectiveCamera, Vector3Tuple } from "three";
|
import type { PerspectiveCamera, Vector3Tuple } from 'three';
|
||||||
import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/controls/OrbitControls.js";
|
import type { OrbitControls as OrbitControlsType } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
|
||||||
let camera = $state<PerspectiveCamera>();
|
let camera = $state<PerspectiveCamera>();
|
||||||
let controls = $state<OrbitControlsType>();
|
let controls = $state<OrbitControlsType>();
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
const cameraTransform = localStore<{
|
const cameraTransform = localStore<{
|
||||||
camera: Vector3Tuple;
|
camera: Vector3Tuple;
|
||||||
target: Vector3Tuple;
|
target: Vector3Tuple;
|
||||||
}>("nodes.camera.transform", {
|
}>('nodes.camera.transform', {
|
||||||
camera: [10, 10, 10],
|
camera: [10, 10, 10],
|
||||||
target: [0, 0, 0],
|
target: [0, 0, 0]
|
||||||
});
|
});
|
||||||
|
|
||||||
function saveCameraState() {
|
function saveCameraState() {
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
if (tPos.some((v) => isNaN(v)) || cPos.some((v) => isNaN(v))) return;
|
if (tPos.some((v) => isNaN(v)) || cPos.some((v) => isNaN(v))) return;
|
||||||
$cameraTransform = {
|
$cameraTransform = {
|
||||||
camera: cPos,
|
camera: cPos,
|
||||||
target: tPos,
|
target: tPos
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +54,13 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (
|
if (
|
||||||
center &&
|
center
|
||||||
controls &&
|
&& controls
|
||||||
centerCamera &&
|
&& centerCamera
|
||||||
(center.x !== controls.target.x ||
|
&& (center.x !== controls.target.x
|
||||||
center.y !== controls.target.y ||
|
|| center.y !== controls.target.y
|
||||||
center.z !== controls.target.z) &&
|
|| center.z !== controls.target.z)
|
||||||
!isRunning
|
&& !isRunning
|
||||||
) {
|
) {
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
task.start();
|
task.start();
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
<script lang="ts">
|
<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 {
|
import {
|
||||||
Grid,
|
|
||||||
MeshLineGeometry,
|
|
||||||
MeshLineMaterial,
|
|
||||||
Text,
|
|
||||||
} from "@threlte/extras";
|
|
||||||
import {
|
|
||||||
type Group,
|
|
||||||
type BufferGeometry,
|
|
||||||
Vector3,
|
|
||||||
type Vector3Tuple,
|
|
||||||
Box3,
|
Box3,
|
||||||
|
type BufferGeometry,
|
||||||
|
type Group,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
} from "three";
|
Vector3,
|
||||||
import { appSettings } from "../settings/app-settings.svelte";
|
type Vector3Tuple
|
||||||
import Camera from "./Camera.svelte";
|
} from 'three';
|
||||||
import { colors } from "$lib/graph-interface/graph/colors.svelte";
|
import { appSettings } from '../settings/app-settings.svelte';
|
||||||
|
import Camera from './Camera.svelte';
|
||||||
|
|
||||||
const { renderStage, invalidate: _invalidate } = useThrelte();
|
const { renderStage, invalidate: _invalidate } = useThrelte();
|
||||||
|
|
||||||
@@ -32,7 +27,7 @@
|
|||||||
lines,
|
lines,
|
||||||
centerCamera,
|
centerCamera,
|
||||||
fps = $bindable(),
|
fps = $bindable(),
|
||||||
scene = $bindable(),
|
scene = $bindable()
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let geometries = $state.raw<BufferGeometry[]>([]);
|
let geometries = $state.raw<BufferGeometry[]>([]);
|
||||||
@@ -43,13 +38,13 @@
|
|||||||
fps.push(1 / delta);
|
fps.push(1 / delta);
|
||||||
fps = fps.slice(-100);
|
fps = fps.slice(-100);
|
||||||
},
|
},
|
||||||
{ stage: renderStage, autoInvalidate: false },
|
{ stage: renderStage, autoInvalidate: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const invalidate = function () {
|
export const invalidate = function() {
|
||||||
if (scene) {
|
if (scene) {
|
||||||
const geos: BufferGeometry[] = [];
|
const geos: BufferGeometry[] = [];
|
||||||
scene.traverse(function (child) {
|
scene.traverse(function(child) {
|
||||||
if (isMesh(child)) {
|
if (isMesh(child)) {
|
||||||
geos.push(child.geometry);
|
geos.push(child.geometry);
|
||||||
}
|
}
|
||||||
@@ -67,17 +62,29 @@
|
|||||||
_invalidate();
|
_invalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
function isMesh(child: Mesh | any): child is Mesh {
|
function isMesh(child: unknown): child is Mesh {
|
||||||
return child.isObject3D && "material" in child;
|
return (
|
||||||
|
child !== null
|
||||||
|
&& typeof child === 'object'
|
||||||
|
&& 'isObject3D' in child
|
||||||
|
&& child.isObject3D === true
|
||||||
|
&& 'material' in child
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMatCapMaterial(material: any): material is MeshBasicMaterial {
|
function isMatCapMaterial(material: unknown): material is MeshBasicMaterial {
|
||||||
return material.isMaterial && "matcap" in material;
|
return (
|
||||||
|
material !== null
|
||||||
|
&& typeof material === 'object'
|
||||||
|
&& 'isMaterial' in material
|
||||||
|
&& material.isMaterial === true
|
||||||
|
&& 'matcap' in material
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const wireframe = appSettings.value.debug.wireframe;
|
const wireframe = appSettings.value.debug.wireframe;
|
||||||
scene.traverse(function (child) {
|
scene.traverse(function(child) {
|
||||||
if (isMesh(child) && isMatCapMaterial(child.material) && child.visible) {
|
if (isMesh(child) && isMatCapMaterial(child.material) && child.visible) {
|
||||||
child.material.wireframe = wireframe;
|
child.material.wireframe = wireframe;
|
||||||
}
|
}
|
||||||
@@ -89,7 +96,7 @@
|
|||||||
return [
|
return [
|
||||||
geo.attributes.position.array[i],
|
geo.attributes.position.array[i],
|
||||||
geo.attributes.position.array[i + 1],
|
geo.attributes.position.array[i + 1],
|
||||||
geo.attributes.position.array[i + 2],
|
geo.attributes.position.array[i + 2]
|
||||||
] as Vector3Tuple;
|
] as Vector3Tuple;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -98,12 +105,12 @@
|
|||||||
|
|
||||||
{#if appSettings.value.showGrid}
|
{#if appSettings.value.showGrid}
|
||||||
<Grid
|
<Grid
|
||||||
cellColor={colors["outline"]}
|
cellColor={colors['outline']}
|
||||||
cellThickness={0.7}
|
cellThickness={0.7}
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
sectionThickness={0.7}
|
sectionThickness={0.7}
|
||||||
sectionDistance={2}
|
sectionDistance={2}
|
||||||
sectionColor={colors["outline"]}
|
sectionColor={colors['outline']}
|
||||||
fadeDistance={50}
|
fadeDistance={50}
|
||||||
fadeStrength={10}
|
fadeStrength={10}
|
||||||
fadeOrigin={new Vector3(0, 0, 0)}
|
fadeOrigin={new Vector3(0, 0, 0)}
|
||||||
@@ -112,9 +119,9 @@
|
|||||||
|
|
||||||
<T.Group>
|
<T.Group>
|
||||||
{#if geometries}
|
{#if geometries}
|
||||||
{#each geometries as geo}
|
{#each geometries as geo (geo.id)}
|
||||||
{#if appSettings.value.debug.showIndices}
|
{#if appSettings.value.debug.showIndices}
|
||||||
{#each geo.attributes.position.array as _, i}
|
{#each geo.attributes.position.array, i (i)}
|
||||||
{#if i % 3 === 0}
|
{#if i % 3 === 0}
|
||||||
<Text fontSize={0.25} position={getPosition(geo, i)} />
|
<Text fontSize={0.25} position={getPosition(geo, i)} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -134,7 +141,7 @@
|
|||||||
</T.Group>
|
</T.Group>
|
||||||
|
|
||||||
{#if appSettings.value.debug.showStemLines && lines}
|
{#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>
|
<T.Mesh>
|
||||||
<MeshLineGeometry points={line} />
|
<MeshLineGeometry points={line} />
|
||||||
<MeshLineMaterial width={0.1} color="red" depthTest={false} />
|
<MeshLineMaterial width={0.1} color="red" depthTest={false} />
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Canvas } from "@threlte/core";
|
import SmallPerformanceViewer from '$lib/performance/SmallPerformanceViewer.svelte';
|
||||||
import Scene from "./Scene.svelte";
|
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||||
import { Vector3 } from "three";
|
import { decodeFloat, splitNestedArray } from '@nodarium/utils';
|
||||||
import { decodeFloat, splitNestedArray } from "@nodarium/utils";
|
import type { PerformanceStore } from '@nodarium/utils';
|
||||||
import type { PerformanceStore } from "@nodarium/utils";
|
import { Canvas } from '@threlte/core';
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import { Vector3 } from 'three';
|
||||||
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
|
import { type Group, MeshMatcapMaterial, TextureLoader } from 'three';
|
||||||
import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
|
import { createGeometryPool, createInstancedGeometryPool } from './geometryPool';
|
||||||
import {
|
import Scene from './Scene.svelte';
|
||||||
createGeometryPool,
|
|
||||||
createInstancedGeometryPool,
|
|
||||||
} from "./geometryPool";
|
|
||||||
|
|
||||||
const loader = new TextureLoader();
|
const loader = new TextureLoader();
|
||||||
const matcap = loader.load("/matcap_green.jpg");
|
const matcap = loader.load('/matcap_green.jpg');
|
||||||
matcap.colorSpace = "srgb";
|
matcap.colorSpace = 'srgb';
|
||||||
const material = new MeshMatcapMaterial({
|
const material = new MeshMatcapMaterial({
|
||||||
color: 0xffffff,
|
color: 0xffffff,
|
||||||
matcap,
|
matcap
|
||||||
});
|
});
|
||||||
|
|
||||||
let sceneComponent = $state<ReturnType<typeof Scene>>();
|
let sceneComponent = $state<ReturnType<typeof Scene>>();
|
||||||
@@ -34,7 +31,7 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
totalFaces: meshes.totalFaces + faces.totalFaces,
|
totalFaces: meshes.totalFaces + faces.totalFaces,
|
||||||
totalVertices: meshes.totalVertices + faces.totalVertices,
|
totalVertices: meshes.totalVertices + faces.totalVertices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +61,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const update = function update(result: Int32Array) {
|
export const update = function update(result: Int32Array) {
|
||||||
console.log({ result });
|
perf.addPoint('split-result');
|
||||||
perf.addPoint("split-result");
|
|
||||||
const inputs = splitNestedArray(result);
|
const inputs = splitNestedArray(result);
|
||||||
perf.endPoint();
|
perf.endPoint();
|
||||||
|
|
||||||
if (appSettings.value.debug.showStemLines) {
|
if (appSettings.value.debug.showStemLines) {
|
||||||
perf.addPoint("create-lines");
|
perf.addPoint('create-lines');
|
||||||
lines = inputs
|
lines = inputs
|
||||||
.map((input) => {
|
.map((input) => {
|
||||||
if (input[0] === 0) {
|
if (input[0] === 0) {
|
||||||
@@ -81,13 +77,13 @@
|
|||||||
perf.endPoint();
|
perf.endPoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
perf.addPoint("update-geometries");
|
perf.addPoint('update-geometries');
|
||||||
|
|
||||||
const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
|
const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
|
||||||
perf.endPoint();
|
perf.endPoint();
|
||||||
|
|
||||||
perf.addPoint("total-vertices", totalVertices);
|
perf.addPoint('total-vertices', totalVertices);
|
||||||
perf.addPoint("total-faces", totalFaces);
|
perf.addPoint('total-faces', totalFaces);
|
||||||
sceneComponent?.invalidate();
|
sceneComponent?.invalidate();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fastHashArrayBuffer } from "@nodarium/utils";
|
import { fastHashArrayBuffer } from '@nodarium/utils';
|
||||||
import {
|
import {
|
||||||
BufferAttribute,
|
BufferAttribute,
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
@@ -7,14 +7,14 @@ import {
|
|||||||
InstancedMesh,
|
InstancedMesh,
|
||||||
Material,
|
Material,
|
||||||
Matrix4,
|
Matrix4,
|
||||||
Mesh,
|
Mesh
|
||||||
} from "three";
|
} from 'three';
|
||||||
|
|
||||||
function fastArrayHash(arr: Int32Array) {
|
function fastArrayHash(arr: Int32Array) {
|
||||||
const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1);
|
const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1);
|
||||||
const sampleCount = Math.floor(arr.length / sampleDistance);
|
const sampleCount = Math.floor(arr.length / sampleDistance);
|
||||||
|
|
||||||
let hash = new Int32Array(sampleCount);
|
const hash = new Int32Array(sampleCount);
|
||||||
|
|
||||||
for (let i = 0; i < sampleCount; i++) {
|
for (let i = 0; i < sampleCount; i++) {
|
||||||
const index = i * sampleDistance;
|
const index = i * sampleDistance;
|
||||||
@@ -28,18 +28,18 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
|||||||
const scene = new Group();
|
const scene = new Group();
|
||||||
parentScene.add(scene);
|
parentScene.add(scene);
|
||||||
|
|
||||||
let meshes: Mesh[] = [];
|
const meshes: Mesh[] = [];
|
||||||
|
|
||||||
let totalVertices = 0;
|
let totalVertices = 0;
|
||||||
let totalFaces = 0;
|
let totalFaces = 0;
|
||||||
|
|
||||||
function updateSingleGeometry(
|
function updateSingleGeometry(
|
||||||
data: Int32Array,
|
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) {
|
if (existingMesh) {
|
||||||
existingMesh.visible = true;
|
existingMesh.visible = true;
|
||||||
}
|
}
|
||||||
@@ -65,8 +65,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
|||||||
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
||||||
index = index + vertexCount * 3;
|
index = index + vertexCount * 3;
|
||||||
|
|
||||||
let posAttribute = geometry.getAttribute(
|
const posAttribute = geometry.getAttribute(
|
||||||
"position",
|
'position'
|
||||||
) as BufferAttribute | null;
|
) as BufferAttribute | null;
|
||||||
|
|
||||||
if (posAttribute && posAttribute.count === vertexCount) {
|
if (posAttribute && posAttribute.count === vertexCount) {
|
||||||
@@ -74,8 +74,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
|||||||
posAttribute.needsUpdate = true;
|
posAttribute.needsUpdate = true;
|
||||||
} else {
|
} else {
|
||||||
geometry.setAttribute(
|
geometry.setAttribute(
|
||||||
"position",
|
'position',
|
||||||
new Float32BufferAttribute(vertices, 3),
|
new Float32BufferAttribute(vertices, 3)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,27 +83,27 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
|||||||
index = index + vertexCount * 3;
|
index = index + vertexCount * 3;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
geometry.userData?.faceCount !== faceCount ||
|
geometry.userData?.faceCount !== faceCount
|
||||||
geometry.userData?.vertexCount !== vertexCount
|
|| geometry.userData?.vertexCount !== vertexCount
|
||||||
) {
|
) {
|
||||||
// Add data to geometry
|
// Add data to geometry
|
||||||
geometry.setIndex([...indices]);
|
geometry.setIndex([...indices]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalsAttribute = geometry.getAttribute(
|
const normalsAttribute = geometry.getAttribute(
|
||||||
"normal",
|
'normal'
|
||||||
) as BufferAttribute | null;
|
) as BufferAttribute | null;
|
||||||
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
||||||
normalsAttribute.set(normals, 0);
|
normalsAttribute.set(normals, 0);
|
||||||
normalsAttribute.needsUpdate = true;
|
normalsAttribute.needsUpdate = true;
|
||||||
} else {
|
} else {
|
||||||
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
|
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
geometry.userData = {
|
geometry.userData = {
|
||||||
vertexCount,
|
vertexCount,
|
||||||
faceCount,
|
faceCount,
|
||||||
hash,
|
hash
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!existingMesh) {
|
if (!existingMesh) {
|
||||||
@@ -119,7 +119,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
|||||||
totalFaces = 0;
|
totalFaces = 0;
|
||||||
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
|
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
|
||||||
const existingMesh = meshes[i];
|
const existingMesh = meshes[i];
|
||||||
let input = newData[i];
|
const input = newData[i];
|
||||||
if (input) {
|
if (input) {
|
||||||
updateSingleGeometry(input, existingMesh || null);
|
updateSingleGeometry(input, existingMesh || null);
|
||||||
} else if (existingMesh) {
|
} else if (existingMesh) {
|
||||||
@@ -127,13 +127,13 @@ export function createGeometryPool(parentScene: Group, material: Material) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { totalVertices, totalFaces };
|
return { totalVertices, totalFaces };
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInstancedGeometryPool(
|
export function createInstancedGeometryPool(
|
||||||
parentScene: Group,
|
parentScene: Group,
|
||||||
material: Material,
|
material: Material
|
||||||
) {
|
) {
|
||||||
const scene = new Group();
|
const scene = new Group();
|
||||||
parentScene.add(scene);
|
parentScene.add(scene);
|
||||||
@@ -144,11 +144,11 @@ export function createInstancedGeometryPool(
|
|||||||
|
|
||||||
function updateSingleInstance(
|
function updateSingleInstance(
|
||||||
data: Int32Array,
|
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
|
? existingInstance.geometry
|
||||||
: new BufferGeometry();
|
: new BufferGeometry();
|
||||||
|
|
||||||
@@ -169,8 +169,8 @@ export function createInstancedGeometryPool(
|
|||||||
const indices = data.subarray(index, indicesEnd);
|
const indices = data.subarray(index, indicesEnd);
|
||||||
index = indicesEnd;
|
index = indicesEnd;
|
||||||
if (
|
if (
|
||||||
geometry.userData?.faceCount !== faceCount ||
|
geometry.userData?.faceCount !== faceCount
|
||||||
geometry.userData?.vertexCount !== vertexCount
|
|| geometry.userData?.vertexCount !== vertexCount
|
||||||
) {
|
) {
|
||||||
// Add data to geometry
|
// Add data to geometry
|
||||||
geometry.setIndex([...indices]);
|
geometry.setIndex([...indices]);
|
||||||
@@ -179,34 +179,34 @@ export function createInstancedGeometryPool(
|
|||||||
// Vertices
|
// Vertices
|
||||||
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
||||||
index = index + vertexCount * 3;
|
index = index + vertexCount * 3;
|
||||||
let posAttribute = geometry.getAttribute(
|
const posAttribute = geometry.getAttribute(
|
||||||
"position",
|
'position'
|
||||||
) as BufferAttribute | null;
|
) as BufferAttribute | null;
|
||||||
if (posAttribute && posAttribute.count === vertexCount) {
|
if (posAttribute && posAttribute.count === vertexCount) {
|
||||||
posAttribute.set(vertices, 0);
|
posAttribute.set(vertices, 0);
|
||||||
posAttribute.needsUpdate = true;
|
posAttribute.needsUpdate = true;
|
||||||
} else {
|
} else {
|
||||||
geometry.setAttribute(
|
geometry.setAttribute(
|
||||||
"position",
|
'position',
|
||||||
new Float32BufferAttribute(vertices, 3),
|
new Float32BufferAttribute(vertices, 3)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
|
||||||
index = index + vertexCount * 3;
|
index = index + vertexCount * 3;
|
||||||
const normalsAttribute = geometry.getAttribute(
|
const normalsAttribute = geometry.getAttribute(
|
||||||
"normal",
|
'normal'
|
||||||
) as BufferAttribute | null;
|
) as BufferAttribute | null;
|
||||||
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
||||||
normalsAttribute.set(normals, 0);
|
normalsAttribute.set(normals, 0);
|
||||||
normalsAttribute.needsUpdate = true;
|
normalsAttribute.needsUpdate = true;
|
||||||
} else {
|
} else {
|
||||||
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
|
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existingInstance &&
|
existingInstance
|
||||||
instanceCount > existingInstance.geometry.userData.count
|
&& instanceCount > existingInstance.geometry.userData.count
|
||||||
) {
|
) {
|
||||||
scene.remove(existingInstance);
|
scene.remove(existingInstance);
|
||||||
instances.splice(instances.indexOf(existingInstance), 1);
|
instances.splice(instances.indexOf(existingInstance), 1);
|
||||||
@@ -226,12 +226,12 @@ export function createInstancedGeometryPool(
|
|||||||
const matrices = new Float32Array(
|
const matrices = new Float32Array(
|
||||||
data.buffer,
|
data.buffer,
|
||||||
index * 4,
|
index * 4,
|
||||||
instanceCount * 16,
|
instanceCount * 16
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < instanceCount; i++) {
|
for (let i = 0; i < instanceCount; i++) {
|
||||||
const matrix = new Matrix4().fromArray(
|
const matrix = new Matrix4().fromArray(
|
||||||
matrices.subarray(i * 16, i * 16 + 16),
|
matrices.subarray(i * 16, i * 16 + 16)
|
||||||
);
|
);
|
||||||
existingInstance.setMatrixAt(i, matrix);
|
existingInstance.setMatrixAt(i, matrix);
|
||||||
}
|
}
|
||||||
@@ -241,9 +241,9 @@ export function createInstancedGeometryPool(
|
|||||||
faceCount,
|
faceCount,
|
||||||
count: Math.max(
|
count: Math.max(
|
||||||
instanceCount,
|
instanceCount,
|
||||||
existingInstance.geometry.userData.count || 0,
|
existingInstance.geometry.userData.count || 0
|
||||||
),
|
),
|
||||||
hash,
|
hash
|
||||||
};
|
};
|
||||||
|
|
||||||
existingInstance.instanceMatrix.needsUpdate = true;
|
existingInstance.instanceMatrix.needsUpdate = true;
|
||||||
@@ -255,7 +255,7 @@ export function createInstancedGeometryPool(
|
|||||||
totalFaces = 0;
|
totalFaces = 0;
|
||||||
for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
|
for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
|
||||||
const existingMesh = instances[i];
|
const existingMesh = instances[i];
|
||||||
let input = newData[i];
|
const input = newData[i];
|
||||||
if (input) {
|
if (input) {
|
||||||
updateSingleInstance(input, existingMesh || null);
|
updateSingleInstance(input, existingMesh || null);
|
||||||
} else if (existingMesh) {
|
} else if (existingMesh) {
|
||||||
@@ -263,6 +263,6 @@ export function createInstancedGeometryPool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { totalVertices, totalFaces };
|
return { totalVertices, totalFaces };
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user