Compare commits
4 Commits
main
...
aea2cbceba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea2cbceba
|
||
|
|
cb89b16dd8
|
||
|
|
4217621574
|
||
|
|
22f285eaff
|
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://dprint.dev/schemas/v0.json",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100,
|
|
||||||
"typescript": {
|
|
||||||
// https://dprint.dev/plugins/typescript/config/
|
|
||||||
"quoteStyle": "preferSingle",
|
|
||||||
"trailingCommas": "never",
|
|
||||||
},
|
|
||||||
"json": {
|
|
||||||
// https://dprint.dev/plugins/json/config/
|
|
||||||
},
|
|
||||||
"markdown": {},
|
|
||||||
"toml": {},
|
|
||||||
"dockerfile": {},
|
|
||||||
"markup": {
|
|
||||||
// https://dprint.dev/plugins/markup_fmt/config/
|
|
||||||
"scriptIndent": true,
|
|
||||||
"styleIndent": true,
|
|
||||||
},
|
|
||||||
"yaml": {},
|
|
||||||
"exec": {
|
|
||||||
"cwd": "${configDir}",
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"command": "rustfmt",
|
|
||||||
"exts": [
|
|
||||||
"rs",
|
|
||||||
],
|
|
||||||
"cacheKeyFiles": [
|
|
||||||
"rustfmt.toml",
|
|
||||||
"rust-toolchain.toml",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"excludes": [
|
|
||||||
"**/node_modules",
|
|
||||||
"**/build",
|
|
||||||
"**/.svelte-kit",
|
|
||||||
"**/package",
|
|
||||||
"**/*-lock.yaml",
|
|
||||||
"**/yaml.lock",
|
|
||||||
"**/.DS_Store",
|
|
||||||
"**/.pnpm-store",
|
|
||||||
"**/.cargo",
|
|
||||||
"**/target",
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"https://plugins.dprint.dev/typescript-0.95.15.wasm",
|
|
||||||
"https://plugins.dprint.dev/json-0.21.1.wasm",
|
|
||||||
"https://plugins.dprint.dev/markdown-0.21.1.wasm",
|
|
||||||
"https://plugins.dprint.dev/toml-0.7.0.wasm",
|
|
||||||
"https://plugins.dprint.dev/dockerfile-0.3.3.wasm",
|
|
||||||
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
|
|
||||||
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm",
|
|
||||||
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 39 KiB |
@@ -1,45 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
commit_message=$(git log -1 --pretty=%B | tr -d '\n' | sed 's/"/\\"/g')
|
|
||||||
|
|
||||||
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": "${commit_message}",
|
|
||||||
"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
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/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 ""
|
|
||||||
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. Setup GPG signing
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
echo "$BOT_PGP_PRIVATE_KEY" | base64 -d | gpg --batch --import
|
|
||||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
|
||||||
|
|
||||||
export GPG_TTY=$(tty)
|
|
||||||
echo "allow-loopback-pinentry" >>~/.gnupg/gpg-agent.conf
|
|
||||||
gpg-connect-agent reloadagent /bye
|
|
||||||
|
|
||||||
git config user.name "nodarium-bot"
|
|
||||||
git config user.email "nodarium-bot@max-richter.dev"
|
|
||||||
git config --global user.signingkey "$GPG_KEY_ID"
|
|
||||||
git config --global commit.gpgsign true
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
# 6. Create release commit
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
git add CHANGELOG.md $(git ls-files '**/package.json')
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
echo "✅ Release process for $TAG complete"
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/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}"
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: 📊 Benchmark the Runtime
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["*"]
|
|
||||||
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:bce06da456e3c008851ac006033cfff256015a47
|
|
||||||
|
|
||||||
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: 🛠️Build Nodes
|
|
||||||
run: pnpm build:nodes
|
|
||||||
|
|
||||||
- name: 🏃 Execute Runtime
|
|
||||||
run: pnpm run --filter @nodarium/app bench
|
|
||||||
|
|
||||||
- name: 📤 Upload Benchmark Results
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: benchmark-data
|
|
||||||
path: app/benchmark/out/
|
|
||||||
compression: 9
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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 }}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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:bce06da456e3c008851ac006033cfff256015a47
|
|
||||||
|
|
||||||
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: |
|
|
||||||
pnpm lint
|
|
||||||
pnpm format:check
|
|
||||||
pnpm check
|
|
||||||
pnpm build
|
|
||||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" pnpm test
|
|
||||||
|
|
||||||
- name: 🚀 Create Release Commit
|
|
||||||
if: gitea.ref_type == 'tag'
|
|
||||||
run: ./.gitea/scripts/create-release.sh
|
|
||||||
env:
|
|
||||||
BOT_PGP_PRIVATE_KEY: ${{ secrets.BOT_PGP_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
- name: 🛠️ Build
|
|
||||||
run: ./.gitea/scripts/build.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
Normal file
1
.github/graphics/nodes.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
38
.github/workflows/deploy.yaml
vendored
Normal file
38
.github/workflows/deploy.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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 }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,3 @@ node_modules/
|
|||||||
# Added by cargo
|
# Added by cargo
|
||||||
|
|
||||||
/target
|
/target
|
||||||
.direnv/
|
|
||||||
|
|
||||||
.pnpm-store/
|
|
||||||
|
|||||||
198
CHANGELOG.md
198
CHANGELOG.md
@@ -1,198 +0,0 @@
|
|||||||
# v0.0.5 (2026-02-13)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Implement debug node with full runtime integration, wildcard (`*`) inputs, variable-height nodes and parameters, and a quick-connect shortcut.
|
|
||||||
- Add color-coded node sockets and edges to visually indicate data types.
|
|
||||||
- Recursively merge `localState` with the initial state to safely handle outdated settings stored in `localStorage` when the settings schema changes.
|
|
||||||
- Clamp the Add Menu to the viewport.
|
|
||||||
- Add application favicon.
|
|
||||||
- Consolidate all developer settings into a single **Advanced Mode** setting.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Fix InputNumber arrow visibility in the light theme.
|
|
||||||
- Correct changelog formatting issues.
|
|
||||||
|
|
||||||
## Chores
|
|
||||||
|
|
||||||
- Add `pnpm qa` pre-commit command.
|
|
||||||
- Run linting and type checks before build in CI.
|
|
||||||
- Sign release commits with a PGP key.
|
|
||||||
- General formatting, lint/type cleanup, test snapshot updates, and `.gitignore` maintenance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- [f16ba26](https://git.max-richter.dev/max/nodarium/commit/f16ba2601ff0e8f0f4454e24689499112a2a257a) fix(ci): still trying to get gpg to work
|
|
||||||
- [cc6b832](https://git.max-richter.dev/max/nodarium/commit/cc6b832f1576356e5453ee4289b02f854152ff9a) fix(ci): trying to get gpg to work
|
|
||||||
- [dd5fd5b](https://git.max-richter.dev/max/nodarium/commit/dd5fd5bf1715d371566bd40419b72ca05e63401e) fix(ci): better add updates to package.json
|
|
||||||
- [38d0fff](https://git.max-richter.dev/max/nodarium/commit/38d0fffcf4ca0a346857c3658ccefdfcdf16e217) chore: update ci image
|
|
||||||
- [bce06da](https://git.max-richter.dev/max/nodarium/commit/bce06da456e3c008851ac006033cfff256015a47) ci: add gpg-agent to ci image
|
|
||||||
- [af585d5](https://git.max-richter.dev/max/nodarium/commit/af585d56ec825662961c8796226ed9d8cb900795) feat: use new ci image with gpg
|
|
||||||
- [0aa73a2](https://git.max-richter.dev/max/nodarium/commit/0aa73a27c1f23bea177ecc66034f8e0384c29a8e) feat: install gpg in ci image
|
|
||||||
- [c1ae702](https://git.max-richter.dev/max/nodarium/commit/c1ae70282cb5d58527180614a80823d80ca478c5) feat: add color to sockets
|
|
||||||
- [4c7b03d](https://git.max-richter.dev/max/nodarium/commit/4c7b03dfb82174317d8ba01f4725af804201154d) feat: add gradient mesh line
|
|
||||||
- [144e8cc](https://git.max-richter.dev/max/nodarium/commit/144e8cc797cfcc5a7a1fd9a0a2098dc99afb6170) fix: correctly highlight possible outputs
|
|
||||||
- [12ff9c1](https://git.max-richter.dev/max/nodarium/commit/12ff9c151873d253ed2e54dcf56aa9c9c4716c7c) Merge pull request 'feat/debug-node' (#41) from feat/debug-node into main
|
|
||||||
- [8d3ffe8](https://git.max-richter.dev/max/nodarium/commit/8d3ffe84ab9ca9e6d6d28333752e34da878fd3ea) Merge branch 'main' into feat/debug-node
|
|
||||||
- [95ec93e](https://git.max-richter.dev/max/nodarium/commit/95ec93eeada9bf062e01e1e77b67b8f0343a51bf) feat: better handle ctrl+shift clicks and selections
|
|
||||||
- [d39185e](https://git.max-richter.dev/max/nodarium/commit/d39185efafc360f49ab9437c0bad1f64665df167) feat: add "pnpm qa" command to check before commit
|
|
||||||
- [81580cc](https://git.max-richter.dev/max/nodarium/commit/81580ccd8c1db30ce83433c4c4df84bd660d3460) fix: cleanup some type errors
|
|
||||||
- [bf6f632](https://git.max-richter.dev/max/nodarium/commit/bf6f632d2772c3da812d5864c401f17e1aa8666a) feat: add shortcut to quick connect to debug
|
|
||||||
- [e098be6](https://git.max-richter.dev/max/nodarium/commit/e098be60135f57cf863904a58489e032ed27e8b4) fix: also execute all nodes before debug node
|
|
||||||
- [ec13850](https://git.max-richter.dev/max/nodarium/commit/ec13850e1c0ca5846da614d25887ff492cf8be04) fix: make debug node work with runtime
|
|
||||||
- [15e08a8](https://git.max-richter.dev/max/nodarium/commit/15e08a816339bdf9de9ecb6a57a7defff42dbe8c) feat: implement debug node
|
|
||||||
- [48cee58](https://git.max-richter.dev/max/nodarium/commit/48cee58ad337c1c6c59a0eb55bf9b5ecd16b99d0) chore: update test snapshots
|
|
||||||
- [3235cae](https://git.max-richter.dev/max/nodarium/commit/3235cae9049e193c242b6091cee9f01e67ee850e) chore: fix lint and typecheck errors
|
|
||||||
- [3f44072](https://git.max-richter.dev/max/nodarium/commit/3f440728fc8a94d59022bb545f418be049a1f1ba) feat: implement variable height for node shader
|
|
||||||
- [da09f8b](https://git.max-richter.dev/max/nodarium/commit/da09f8ba1eda5ed347433d37064a3b4ab49e627e) refactor: move debug node into runtime
|
|
||||||
- [ddc3b4c](https://git.max-richter.dev/max/nodarium/commit/ddc3b4ce357ef1c1e8066c0a52151713d0b6ed95) feat: allow variable height node parameters
|
|
||||||
- [2690fc8](https://git.max-richter.dev/max/nodarium/commit/2690fc871291e73d3d028df9668e8fffb1e77476) chore: gitignore pnpm-store
|
|
||||||
- [072ab90](https://git.max-richter.dev/max/nodarium/commit/072ab9063ba56df0673020eb639548f3a8601e04) feat: add initial debug node
|
|
||||||
- [e23cad2](https://git.max-richter.dev/max/nodarium/commit/e23cad254d610e00f196b7fdb4532f36fd735a4b) feat: add "*" datatype for inputs for debug node
|
|
||||||
- [5b5c63c](https://git.max-richter.dev/max/nodarium/commit/5b5c63c1a9c4ef757382bd4452149dc9777693ff) fix(ui): make arrows on inputnumber visible on lighttheme
|
|
||||||
- [c9021f2](https://git.max-richter.dev/max/nodarium/commit/c9021f2383828f2e2b5594d125165bbc8f70b8e7) refactor: merge all dev settings into one setting
|
|
||||||
- [9eecdd4](https://git.max-richter.dev/max/nodarium/commit/9eecdd4fb85dc60b8196101050334e26732c9a34) Merge pull request 'feat: merge localState recursively with initial' (#38) from feat/debug-node into main
|
|
||||||
- [7e71a41](https://git.max-richter.dev/max/nodarium/commit/7e71a41e5229126d404f56598c624709961dbf3b) feat: merge localState recursively with initial
|
|
||||||
- [07cd9e8](https://git.max-richter.dev/max/nodarium/commit/07cd9e84eb51bc02b7fed39c36cf83caba175ad7) feat: clamp AddMenu to viewport
|
|
||||||
- [a31a49a](https://git.max-richter.dev/max/nodarium/commit/a31a49ad503d69f92f2491dd685729060ea49896) ci: lint and typecheck before build
|
|
||||||
- [850d641](https://git.max-richter.dev/max/nodarium/commit/850d641a25cd0c781478c58c117feaf085bdbc62) chore: pnpm format
|
|
||||||
- [ee5ca81](https://git.max-richter.dev/max/nodarium/commit/ee5ca817573b83cacfa3709e0ae88c6263bc39c1) ci: sign release commits with pgp key
|
|
||||||
- [22a1183](https://git.max-richter.dev/max/nodarium/commit/22a11832b861ae8b44e2d374b55d12937ecab247) fix(ci): correctly format changelog
|
|
||||||
- [b5ce572](https://git.max-richter.dev/max/nodarium/commit/b5ce5723fa4a35443df39a9096d0997f808f0b4f) chore: format favicon svg
|
|
||||||
- [102130c](https://git.max-richter.dev/max/nodarium/commit/102130cc7777ceebcdb3de8466c90cef5b380111) feat: add favicon
|
|
||||||
- [1668a2e](https://git.max-richter.dev/max/nodarium/commit/1668a2e6d59db071ab3da45204c2b7bfcd2150a2) chore: format changelog.md
|
|
||||||
|
|
||||||
# v0.0.4 (2026-02-10)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Added shape and leaf nodes, including rotation support.
|
|
||||||
- Added high-contrast light theme and improved overall node readability.
|
|
||||||
- Enhanced UI with dots background, clearer details, and consistent node coloring.
|
|
||||||
- Improved changelog display and parsing robustness.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Fixed UI issues (backside rendering, missing types, linter errors).
|
|
||||||
- Improved CI handling of commit messages and changelog placement.
|
|
||||||
|
|
||||||
## Chores
|
|
||||||
|
|
||||||
- Simplified CI quality checks.
|
|
||||||
- Updated dprint linters.
|
|
||||||
- Refactored changelog code.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- [51de3ce](https://git.max-richter.dev/max/nodarium/commit/51de3ced133af07b9432e1137068ef43ddfecbc9) fix(ci): update changelog before building
|
|
||||||
- [8d403ba](https://git.max-richter.dev/max/nodarium/commit/8d403ba8039a05b687f050993a6afca7fb743e12) Merge pull request 'feat/shape-node' (#36) from feat/shape-node into main
|
|
||||||
- [6bb3011](https://git.max-richter.dev/max/nodarium/commit/6bb301153ac13c31511b6b28ae95c6e0d4c03e9e) Merge remote-tracking branch 'origin/main' into feat/shape-node
|
|
||||||
- [02eee5f](https://git.max-richter.dev/max/nodarium/commit/02eee5f9bf4b1bc813d5d28673c4d5d77b392a92) fix: disable macro logs in wasm
|
|
||||||
- [4f48a51](https://git.max-richter.dev/max/nodarium/commit/4f48a519a950123390530f1b6040e2430a767745) feat(nodes): add rotation to instance node
|
|
||||||
- [97199ac](https://git.max-richter.dev/max/nodarium/commit/97199ac20fb079d6c157962d1a998d63670d8797) feat(nodes): implement leaf node
|
|
||||||
- [f36f0cb](https://git.max-richter.dev/max/nodarium/commit/f36f0cb2305692c7be60889bcde7f91179e18b81) feat(ui): show circles only when hovering InputShape
|
|
||||||
- [ed3d48e](https://git.max-richter.dev/max/nodarium/commit/ed3d48e07fa6db84bbb24db6dbe044cbc36f049f) fix(runtime): correctly encode 2d shape for wasm nodes
|
|
||||||
- [c610d6c](https://git.max-richter.dev/max/nodarium/commit/c610d6c99152d8233235064b81503c2b0dc4ada8) fix(app): show backside in three instances
|
|
||||||
- [8865b9b](https://git.max-richter.dev/max/nodarium/commit/8865b9b032bdf5a1385b4e9db0b1923f0e224fdd) feat(node): initial leaf / shape nodes
|
|
||||||
- [235ee5d](https://git.max-richter.dev/max/nodarium/commit/235ee5d979fbd70b3e0fb6f09a352218c3ff1e6d) fix(app): wrong linter errors in changelog
|
|
||||||
- [23a4857](https://git.max-richter.dev/max/nodarium/commit/23a48572f3913d91d839873cc155a16139c286a6) feat(app): dots background for node interface
|
|
||||||
- [e89a46e](https://git.max-richter.dev/max/nodarium/commit/e89a46e146e9e95de57ffdf55b05d16d6fe975f4) feat(app): add error page
|
|
||||||
- [cefda41](https://git.max-richter.dev/max/nodarium/commit/cefda41fcf3d5d011c9f7598a4f3f37136602dbd) feat(theme): optimize node readability
|
|
||||||
- [21d0f0d](https://git.max-richter.dev/max/nodarium/commit/21d0f0da5a26492fa68ad4897a9b1d9e88857030) feat: add high-contrast-light theme
|
|
||||||
- [4620245](https://git.max-richter.dev/max/nodarium/commit/46202451ba3eea73bd1bc6ef5129b3e26ee9315c) ci: simplify ci quality checks
|
|
||||||
- [0f4239d](https://git.max-richter.dev/max/nodarium/commit/0f4239d179ddedd3d012ca98b5bc3312afbc8f10) ci: simplify ci quality checks
|
|
||||||
- [d9c9bb5](https://git.max-richter.dev/max/nodarium/commit/d9c9bb5234bc8776daf26be99ba77a2145c70649) fix(theme): allow raw html in head style
|
|
||||||
- [18802fd](https://git.max-richter.dev/max/nodarium/commit/18802fdc10294a58425f052a4fde4bcf4be58caf) fix(ui): add missing types
|
|
||||||
- [b1cbd23](https://git.max-richter.dev/max/nodarium/commit/b1cbd235420c99a11154ef6a899cc7e14faf1c37) feat(app): use same color for node outline and header
|
|
||||||
- [33f10da](https://git.max-richter.dev/max/nodarium/commit/33f10da396fdc13edcb8faaee212280102b24f3a) feat(ui): make details stand out
|
|
||||||
- [af5b3b2](https://git.max-richter.dev/max/nodarium/commit/af5b3b23ba18d73d6abec60949fb0c9edfc25ff8) fix: make sure that CHANGELOG.md is in correct place
|
|
||||||
- [64d75b9](https://git.max-richter.dev/max/nodarium/commit/64d75b9686c494075223a0a318297fe59ec99e81) feat(ui): add InputColor and custom theme
|
|
||||||
- [2e6466c](https://git.max-richter.dev/max/nodarium/commit/2e6466ceca1d2131581d1862e93c756affdf6cd6) chore: update dprint linters
|
|
||||||
- [20d8e2a](https://git.max-richter.dev/max/nodarium/commit/20d8e2abedf0de30299d947575afef9c8ffd61d9) feat(theme): improve light theme a bit
|
|
||||||
- [715e1d0](https://git.max-richter.dev/max/nodarium/commit/715e1d095b8a77feb0cf66bbb444baf0f163adcb) feat(theme): merge edge and connection color
|
|
||||||
- [07e2826](https://git.max-richter.dev/max/nodarium/commit/07e2826f16dafa6a07377c9fb591168fa5c2abcf) feat(ui): improve colors of input shape
|
|
||||||
- [e0ad97b](https://git.max-richter.dev/max/nodarium/commit/e0ad97b003fd8cb4d950c03e5488a5accf6a37d0) feat(ui): highlight circle on hover on InputShape
|
|
||||||
- [93df4a1](https://git.max-richter.dev/max/nodarium/commit/93df4a19ff816e2bdfa093594721f0829f84c9e6) fix(ci): handle newline in commit messages for git.json
|
|
||||||
- [d661a4e](https://git.max-richter.dev/max/nodarium/commit/d661a4e4a9dfa6c9c73b5e24a3edcf56e1bbf48c) feat(ui): improve InputShape ux
|
|
||||||
- [c7f808c](https://git.max-richter.dev/max/nodarium/commit/c7f808ce2d52925425b49f92edf49d9557f8901d) wip
|
|
||||||
- [72d6cd6](https://git.max-richter.dev/max/nodarium/commit/72d6cd6ea2886626823e6e86856f19338c7af3c1) feat(ui): add initial InputShape element
|
|
||||||
- [615f2d3](https://git.max-richter.dev/max/nodarium/commit/615f2d3c4866a9e85f3eca398f3f02100c4df355) feat(ui): allow custom snippets in ui section header
|
|
||||||
- [2fadb68](https://git.max-richter.dev/max/nodarium/commit/2fadb6802de640d692fdab7d654311df0d7b4836) refactor: make changelog code simpler
|
|
||||||
- [9271d3a](https://git.max-richter.dev/max/nodarium/commit/9271d3a7e4cb0cc751b635c2adb518de7b4100c7) fix(app): handle error while parsing commit
|
|
||||||
- [13c83ef](https://git.max-richter.dev/max/nodarium/commit/13c83efdb962a6578ade59f10cc574fef0e17534) fix(app): handle error while parsing changelog
|
|
||||||
- [e44b73b](https://git.max-richter.dev/max/nodarium/commit/e44b73bebfb1cc8e872cd2fa7d8b6ff3565df374) feat: optimize changelog display
|
|
||||||
- [979e9fd](https://git.max-richter.dev/max/nodarium/commit/979e9fd92289eba9f77221c563337c00028e4cf5) feat: improve changelog readbility
|
|
||||||
- [544500e](https://git.max-richter.dev/max/nodarium/commit/544500e7fe9ee14412cef76f3c7a32ba6f291656) chore: remove pgp from changelog
|
|
||||||
- [aaebbc4](https://git.max-richter.dev/max/nodarium/commit/aaebbc4bc082ee93c2317ce45071c9bc61b0b77e) fix: some stuff with ci
|
|
||||||
|
|
||||||
# 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
|
|
||||||
365
Cargo.lock
generated
365
Cargo.lock
generated
@@ -1,88 +1,176 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "box"
|
name = "box"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "branch"
|
name = "branch"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "float"
|
name = "float"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glam"
|
name = "glam"
|
||||||
version = "0.30.10"
|
version = "0.27.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
|
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gravity"
|
name = "gravity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"glam",
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
]
|
"noise",
|
||||||
|
"serde",
|
||||||
[[package]]
|
"serde-wasm-bindgen",
|
||||||
name = "instance"
|
"wasm-bindgen",
|
||||||
version = "0.1.0"
|
"wasm-bindgen-test",
|
||||||
dependencies = [
|
"web-sys",
|
||||||
"glam",
|
|
||||||
"nodarium_macros",
|
|
||||||
"nodarium_utils",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leaf"
|
name = "js-sys"
|
||||||
version = "0.1.0"
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nodarium_macros",
|
"wasm-bindgen",
|
||||||
"nodarium_utils",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "math"
|
name = "math"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "max-plantarium-triangle"
|
||||||
version = "2.7.6"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
dependencies = [
|
||||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
"console_error_panic_hook",
|
||||||
|
"nodarium_macros",
|
||||||
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "max-plantarium-vec3"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"nodarium_macros",
|
||||||
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodarium_instance"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"glam",
|
||||||
|
"nodarium_macros",
|
||||||
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodarium_macros"
|
name = "nodarium_macros"
|
||||||
@@ -92,7 +180,7 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"syn",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -107,19 +195,29 @@ dependencies = [
|
|||||||
name = "nodarium_utils"
|
name = "nodarium_utils"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"glam",
|
"glam",
|
||||||
"noise 0.9.0",
|
"noise",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "noise"
|
name = "nodes-noise"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
"noise 0.9.0",
|
"noise",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -135,35 +233,48 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "output"
|
name = "output"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.105"
|
version = "1.0.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.43"
|
version = "1.0.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -196,84 +307,103 @@ dependencies = [
|
|||||||
name = "random"
|
name = "random"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rotate"
|
name = "rotate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"glam",
|
"glam",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.198"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde-wasm-bindgen"
|
||||||
version = "1.0.228"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.228"
|
version = "1.0.198"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.60",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.149"
|
version = "1.0.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
|
||||||
"zmij",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shape"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"nodarium_macros",
|
|
||||||
"nodarium_utils",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stem"
|
name = "stem"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
"nodarium_macros",
|
"nodarium_macros",
|
||||||
"nodarium_utils",
|
"nodarium_utils",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.114"
|
version = "1.0.109"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -281,30 +411,119 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "triangle"
|
name = "syn"
|
||||||
version = "0.1.0"
|
version = "2.0.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nodarium_macros",
|
"proc-macro2",
|
||||||
"nodarium_utils",
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vec3"
|
name = "wasm-bindgen"
|
||||||
version = "0.1.0"
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nodarium_macros",
|
"cfg-if",
|
||||||
"nodarium_utils",
|
"wasm-bindgen-macro",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "wasm-bindgen-backend"
|
||||||
version = "1.0.15"
|
version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.60",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.60",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-test"
|
||||||
|
version = "0.3.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"scoped-tls",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-bindgen-test-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-test-macro"
|
||||||
|
version = "0.3.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.60",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM node:24-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache --update curl rclone g++
|
||||||
|
|
||||||
|
RUN RUSTUP_URL="https://sh.rustup.rs" \
|
||||||
|
&& curl --silent --show-error --location --fail --retry 3 --proto '=https' --tlsv1.2 --output /tmp/rustup-linux-install.sh $RUSTUP_URL \
|
||||||
|
&& sh /tmp/rustup-linux-install.sh -y
|
||||||
|
|
||||||
|
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 default stable \
|
||||||
|
&& rustup target add wasm32-unknown-unknown \
|
||||||
|
&& cargo install wasm-pack \
|
||||||
|
&& npm i -g pnpm
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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 \
|
|
||||||
gpg=2.2.40-1.1+deb12u2 \
|
|
||||||
gpg-agent=2.2.40-1.1+deb12u2 \
|
|
||||||
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/*
|
|
||||||
37
README.md
37
README.md
@@ -2,17 +2,17 @@ Nodarium
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a>
|
<a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Nodarium is a WebAssembly based visual programming language.
|
Nodarium is a WebAssembly based visual programming language.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<img src=".gitea/graphics/nodes.svg" width="80%"/>
|
<img src=".github/graphics/nodes.svg" width="80%"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Currently this visual programming language is used to develop <https://nodes.max-richter.dev>, a procedural modelling tool for 3d-plants.
|
Currently this visual programming language is used to develop https://nodes.max-richter.dev, a procedural modelling tool for 3d-plants.
|
||||||
|
|
||||||
# Table of contents
|
# Table of contents
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ Currently this visual programming language is used to develop <https://nodes.max
|
|||||||
|
|
||||||
# Developing
|
# Developing
|
||||||
|
|
||||||
### Install prerequisites
|
### Install prerequisites:
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/download)
|
- [Node.js](https://nodejs.org/en/download)
|
||||||
- [pnpm](https://pnpm.io/installation)
|
- [pnpm](https://pnpm.io/installation)
|
||||||
@@ -50,29 +50,4 @@ 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
|
||||||
|
|||||||
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@@ -27,5 +27,3 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
build/
|
build/
|
||||||
|
|
||||||
test-results/
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
FROM jimfx/nodes:latest AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json Cargo.lock Cargo.toml ./
|
|
||||||
|
|
||||||
COPY packages/ ./packages/
|
|
||||||
COPY nodes/ ./nodes/
|
|
||||||
COPY app/package.json ./app/
|
|
||||||
|
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
||||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
|
||||||
--mount=type=cache,target=/app/target \
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
||||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
|
||||||
--mount=type=cache,target=/app/target \
|
|
||||||
pnpm build:nodes && \
|
|
||||||
pnpm --filter @nodarium/app... build
|
|
||||||
|
|
||||||
FROM nginx:alpine AS runner
|
|
||||||
|
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
COPY app/docker/app.conf /etc/nginx/conf.d/app.conf
|
|
||||||
|
|
||||||
COPY --from=builder /app/app/build /app
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
1
app/benchmark/.gitignore
vendored
1
app/benchmark/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
out/
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { NodeDefinition, NodeId, NodeRegistry } from '@nodarium/types';
|
|
||||||
import { createWasmWrapper } from '@nodarium/utils';
|
|
||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
import { resolve } from 'node:path';
|
|
||||||
|
|
||||||
export class BenchmarkRegistry implements NodeRegistry {
|
|
||||||
status: 'loading' | 'ready' | 'error' = 'loading';
|
|
||||||
|
|
||||||
private nodes = new Map<string, NodeDefinition>();
|
|
||||||
|
|
||||||
async load(nodeIds: NodeId[]): Promise<NodeDefinition[]> {
|
|
||||||
const nodes = await Promise.all(nodeIds.map(async id => {
|
|
||||||
const p = resolve('static/nodes/' + id + '.wasm');
|
|
||||||
const file = await readFile(p);
|
|
||||||
const node = createWasmWrapper(file as unknown as ArrayBuffer);
|
|
||||||
const d = node.get_definition();
|
|
||||||
return {
|
|
||||||
...d,
|
|
||||||
execute: node.execute
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
for (const n of nodes) {
|
|
||||||
this.nodes.set(n.id, n);
|
|
||||||
}
|
|
||||||
this.status = 'ready';
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async register(id: string, wasmBuffer: ArrayBuffer): Promise<NodeDefinition> {
|
|
||||||
const wasm = createWasmWrapper(wasmBuffer);
|
|
||||||
const d = wasm.get_definition();
|
|
||||||
const node = {
|
|
||||||
...d,
|
|
||||||
execute: wasm.execute
|
|
||||||
};
|
|
||||||
this.nodes.set(id, node);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNode(id: NodeId | string): NodeDefinition | undefined {
|
|
||||||
return this.nodes.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllNodes(): NodeDefinition[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import type { Graph, Graph as GraphType, NodeId } from '@nodarium/types';
|
|
||||||
import { createLogger, createPerformanceStore } from '@nodarium/utils';
|
|
||||||
import { mkdir, writeFile } from 'node:fs/promises';
|
|
||||||
import { resolve } from 'node:path';
|
|
||||||
import { MemoryRuntimeExecutor } from '../src/lib/runtime/runtime-executor.ts';
|
|
||||||
import { BenchmarkRegistry } from './benchmarkRegistry.ts';
|
|
||||||
import defaultPlantTemplate from './templates/default.json' assert { type: 'json' };
|
|
||||||
import lottaFacesTemplate from './templates/lotta-faces.json' assert { type: 'json' };
|
|
||||||
import plantTemplate from './templates/plant.json' assert { type: 'json' };
|
|
||||||
|
|
||||||
const registry = new BenchmarkRegistry();
|
|
||||||
const r = new MemoryRuntimeExecutor(registry);
|
|
||||||
const perfStore = createPerformanceStore();
|
|
||||||
|
|
||||||
const log = createLogger('bench');
|
|
||||||
|
|
||||||
const templates: Record<string, Graph> = {
|
|
||||||
'plant': plantTemplate as unknown as GraphType,
|
|
||||||
'lotta-faces': lottaFacesTemplate as unknown as GraphType,
|
|
||||||
'default': defaultPlantTemplate as unknown as GraphType
|
|
||||||
};
|
|
||||||
|
|
||||||
async function run(g: GraphType, amount: number) {
|
|
||||||
await registry.load(plantTemplate.nodes.map(n => n.type) as NodeId[]);
|
|
||||||
log.log('loaded ' + g.nodes.length + ' nodes');
|
|
||||||
|
|
||||||
log.log('warming up');
|
|
||||||
|
|
||||||
// Warm up the runtime? maybe this does something?
|
|
||||||
for (let index = 0; index < 10; index++) {
|
|
||||||
await r.execute(g, { randomSeed: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
log.log('executing');
|
|
||||||
r.perf = perfStore;
|
|
||||||
for (let i = 0; i < amount; i++) {
|
|
||||||
r.perf?.startRun();
|
|
||||||
await r.execute(g, { randomSeed: true });
|
|
||||||
r.perf?.stopRun();
|
|
||||||
}
|
|
||||||
log.log('finished');
|
|
||||||
return r.perf.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const outPath = resolve('benchmark/out/');
|
|
||||||
await mkdir(outPath, { recursive: true });
|
|
||||||
for (const key in templates) {
|
|
||||||
log.log('executing ' + key);
|
|
||||||
const perfData = await run(templates[key], 100);
|
|
||||||
await writeFile(resolve(outPath, key + '.json'), JSON.stringify(perfData));
|
|
||||||
await new Promise(res => setTimeout(res, 200));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
{
|
|
||||||
"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,44 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
|
|
||||||
"nodes": [
|
|
||||||
{ "id": 9, "position": [180, 80], "type": "max/plantarium/output", "props": {} },
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"position": [55, 80],
|
|
||||||
"type": "max/plantarium/stem",
|
|
||||||
"props": { "amount": 1, "length": 11, "thickness": 0.71 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 11,
|
|
||||||
"position": [80, 80],
|
|
||||||
"type": "max/plantarium/noise",
|
|
||||||
"props": {
|
|
||||||
"strength": 35,
|
|
||||||
"scale": 4.6,
|
|
||||||
"fixBottom": 1,
|
|
||||||
"directionalStrength": [1, 0.74, 0.083],
|
|
||||||
"depth": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"position": [105, 80],
|
|
||||||
"type": "max/plantarium/branch",
|
|
||||||
"props": {
|
|
||||||
"length": 3,
|
|
||||||
"thickness": 0.6,
|
|
||||||
"amount": 10,
|
|
||||||
"rotation": 180,
|
|
||||||
"offsetSingle": 0.34,
|
|
||||||
"lowestBranch": 0.53,
|
|
||||||
"highestBranch": 1,
|
|
||||||
"depth": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 13,
|
|
||||||
"position": [130, 80],
|
|
||||||
"type": "max/plantarium/noise",
|
|
||||||
"props": {
|
|
||||||
"strength": 8,
|
|
||||||
"scale": 7.7,
|
|
||||||
"fixBottom": 1,
|
|
||||||
"directionalStrength": [1, 0, 1],
|
|
||||||
"depth": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 14,
|
|
||||||
"position": [155, 80],
|
|
||||||
"type": "max/plantarium/gravity",
|
|
||||||
"props": {
|
|
||||||
"strength": 0.11,
|
|
||||||
"scale": 39,
|
|
||||||
"fixBottom": 0,
|
|
||||||
"directionalStrength": [1, 1, 1],
|
|
||||||
"depth": 1,
|
|
||||||
"curviness": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"edges": [
|
|
||||||
[10, 0, 11, "plant"],
|
|
||||||
[11, 0, 12, "plant"],
|
|
||||||
[12, 0, 13, "plant"],
|
|
||||||
[13, 0, 14, "plant"],
|
|
||||||
[14, 0, 9, "input"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
root /app;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,37 +0,0 @@
|
|||||||
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,65 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "@nodarium/app",
|
"name": "@nodarium/app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.5",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"predev": "rm static/CHANGELOG.md && ln -s ../../CHANGELOG.md static/CHANGELOG.md",
|
|
||||||
"build": "svelte-kit sync && vite build",
|
"build": "svelte-kit sync && vite build",
|
||||||
"test:unit": "vitest",
|
"test": "vitest",
|
||||||
"test": "npm run test:unit -- --run && npm run test:e2e",
|
"preview": "vite preview"
|
||||||
"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",
|
|
||||||
"bench": "tsx ./benchmark/index.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodarium/ui": "workspace:*",
|
"@nodarium/registry": "link:../packages/registry",
|
||||||
"@nodarium/utils": "workspace:*",
|
"@nodarium/ui": "link:../packages/ui",
|
||||||
"@sveltejs/kit": "^2.50.2",
|
"@nodarium/utils": "link:../packages/utils",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@sveltejs/kit": "^2.50.0",
|
||||||
"@threlte/core": "8.3.1",
|
"@threlte/core": "8.3.1",
|
||||||
"@threlte/extras": "9.7.1",
|
"@threlte/extras": "9.7.1",
|
||||||
|
"@types/three": "^0.182.0",
|
||||||
|
"@unocss/reset": "^66.6.0",
|
||||||
"comlink": "^4.4.2",
|
"comlink": "^4.4.2",
|
||||||
"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",
|
|
||||||
"three": "^0.182.0"
|
"three": "^0.182.0"
|
||||||
},
|
},
|
||||||
"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",
|
"@nodarium/types": "link:../packages/types",
|
||||||
"@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.7",
|
"@tsconfig/svelte": "^5.0.6",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/three": "^0.182.0",
|
"@unocss/preset-icons": "^66.6.0",
|
||||||
"@vitest/browser-playwright": "^4.0.18",
|
"svelte": "^5.46.4",
|
||||||
"dprint": "^0.51.1",
|
"svelte-check": "^4.3.5",
|
||||||
"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",
|
||||||
"tsx": "^4.21.0",
|
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.54.0",
|
"unocss": "^66.6.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.18",
|
"vitest": "^4.0.17"
|
||||||
"vitest-browser-svelte": "^2.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
1576
app/pnpm-lock.yaml
generated
Normal file
1576
app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@source "../../packages/ui/**/*.svelte";
|
|
||||||
@plugin "@iconify/tailwind4" {
|
|
||||||
prefix: "i";
|
|
||||||
icon-sets: from-folder("custom", "./src/lib/icons");
|
|
||||||
}
|
|
||||||
|
|
||||||
body * {
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
14
app/src/app.d.ts
vendored
14
app/src/app.d.ts
vendored
@@ -1,13 +1,13 @@
|
|||||||
// See https://kit.svelte.dev/docs/types#app
|
// 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,26 +1,29 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.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>
|
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<head>
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<meta charset="utf-8" />
|
||||||
</body>
|
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<script defer src="https://umami.max-richter.dev/script.js" data-website-id="585c442b-0524-4874-8955-f9853b44b17e"></script>
|
||||||
|
%sveltekit.head%
|
||||||
|
<title>Nodes</title>
|
||||||
|
<script>
|
||||||
|
var store = localStorage.getItem("node-settings");
|
||||||
|
if (store) {
|
||||||
|
try {
|
||||||
|
var value = JSON.parse(store);
|
||||||
|
var themes = ["dark", "light", "catppuccin"];
|
||||||
|
if (themes[value.theme]) {
|
||||||
|
document.documentElement.classList.add("theme-" + themes[value.theme]);
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
import { PUBLIC_ANALYTIC_SCRIPT } from '$env/static/public';
|
|
||||||
export const ANALYTIC_SCRIPT = PUBLIC_ANALYTIC_SCRIPT;
|
|
||||||
@@ -11,7 +11,6 @@ uniform vec3 camPos;
|
|||||||
uniform vec2 zoomLimits;
|
uniform vec2 zoomLimits;
|
||||||
uniform vec3 backgroundColor;
|
uniform vec3 backgroundColor;
|
||||||
uniform vec3 lineColor;
|
uniform vec3 lineColor;
|
||||||
uniform int gridType; // 0 = grid lines, 1 = dots
|
|
||||||
|
|
||||||
// Anti-aliased step: threshold in the same units as `value`
|
// Anti-aliased step: threshold in the same units as `value`
|
||||||
float aaStep(float threshold, float value, float deriv) {
|
float aaStep(float threshold, float value, float deriv) {
|
||||||
@@ -79,51 +78,35 @@ void main(void) {
|
|||||||
float ux = (vUv.x - 0.5) * width + cx * cz;
|
float ux = (vUv.x - 0.5) * width + cx * cz;
|
||||||
float uy = (vUv.y - 0.5) * height - cy * cz;
|
float uy = (vUv.y - 0.5) * height - cy * cz;
|
||||||
|
|
||||||
if(gridType == 0) {
|
// extra small grid
|
||||||
// extra small grid
|
float m1 = grid(ux, uy, divisions * 4.0, thickness * 4.0) * 0.9;
|
||||||
float m1 = grid(ux, uy, divisions * 4.0, thickness * 4.0) * 0.9;
|
float m2 = grid(ux, uy, divisions * 16.0, thickness * 16.0) * 0.5;
|
||||||
float m2 = grid(ux, uy, divisions * 16.0, thickness * 16.0) * 0.5;
|
float xsmall = max(m1, m2);
|
||||||
float xsmall = max(m1, m2);
|
|
||||||
|
float s3 = circle_grid(ux, uy, cz / 1.6, 1.0) * 0.5;
|
||||||
float s3 = circle_grid(ux, uy, cz / 1.6, 1.0) * 0.5;
|
xsmall = max(xsmall, s3);
|
||||||
xsmall = max(xsmall, s3);
|
|
||||||
|
|
||||||
// small grid
|
// small grid
|
||||||
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
|
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
|
||||||
float c2 = grid(ux, uy, divisions * 2.0, thickness * 2.0) * 0.5;
|
float c2 = grid(ux, uy, divisions * 2.0, thickness * 2.0) * 0.5;
|
||||||
float small = max(c1, c2);
|
float small = max(c1, c2);
|
||||||
|
|
||||||
float s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
|
float s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
|
||||||
small = max(small, s1);
|
small = max(small, s1);
|
||||||
|
|
||||||
// large grid
|
// large grid
|
||||||
float c3 = grid(ux, uy, divisions / 8.0, thickness / 8.0) * 0.5;
|
float c3 = grid(ux, uy, divisions / 8.0, thickness / 8.0) * 0.5;
|
||||||
float c4 = grid(ux, uy, divisions / 2.0, thickness / 4.0) * 0.4;
|
float c4 = grid(ux, uy, divisions / 2.0, thickness / 4.0) * 0.4;
|
||||||
float large = max(c3, c4);
|
float large = max(c3, c4);
|
||||||
|
|
||||||
float s2 = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
|
float s2 = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
|
||||||
large = max(large, s2);
|
large = max(large, s2);
|
||||||
|
|
||||||
float c = mix(large, small, min(nz * 2.0 + 0.05, 1.0));
|
float c = mix(large, small, min(nz * 2.0 + 0.05, 1.0));
|
||||||
c = mix(c, xsmall, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
|
c = mix(c, xsmall, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
|
||||||
|
|
||||||
vec3 color = mix(backgroundColor, lineColor, c);
|
vec3 color = mix(backgroundColor, lineColor, c);
|
||||||
|
|
||||||
gl_FragColor = vec4(color, 1.0);
|
|
||||||
} else {
|
|
||||||
float large = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
|
|
||||||
|
|
||||||
float medium = circle_grid(ux, uy, cz * 10.0, 1.0) * 0.6;
|
|
||||||
|
|
||||||
float small = circle_grid(ux, uy, cz * 2.5, 1.0) * 0.8;
|
|
||||||
|
|
||||||
float c = mix(large, medium, min(nz * 2.0 + 0.05, 1.0));
|
|
||||||
c = mix(c, small, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
|
|
||||||
|
|
||||||
vec3 color = mix(backgroundColor, lineColor, c);
|
|
||||||
|
|
||||||
gl_FragColor = vec4(color, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
import { T } from "@threlte/core";
|
||||||
import { T } from '@threlte/core';
|
import BackgroundVert from "./Background.vert";
|
||||||
import { colors } from '../graph/colors.svelte';
|
import BackgroundFrag from "./Background.frag";
|
||||||
import BackgroundFrag from './Background.frag';
|
import { colors } from "../graph/colors.svelte";
|
||||||
import BackgroundVert from './Background.vert';
|
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
minZoom?: number;
|
minZoom: number;
|
||||||
maxZoom?: number;
|
maxZoom: number;
|
||||||
cameraPosition?: [number, number, number];
|
cameraPosition: [number, number, number];
|
||||||
width?: number;
|
width: number;
|
||||||
height?: number;
|
height: number;
|
||||||
type?: 'grid' | 'dots' | 'none';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -20,17 +19,8 @@
|
|||||||
cameraPosition = [0, 1, 0],
|
cameraPosition = [0, 1, 0],
|
||||||
width = globalThis?.innerWidth || 100,
|
width = globalThis?.innerWidth || 100,
|
||||||
height = globalThis?.innerHeight || 100,
|
height = globalThis?.innerHeight || 100,
|
||||||
type = 'grid'
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const typeMap = new Map([
|
|
||||||
['grid', 0],
|
|
||||||
['dots', 1],
|
|
||||||
['none', 2]
|
|
||||||
]);
|
|
||||||
|
|
||||||
const gridType = $derived(typeMap.get(type) || 0);
|
|
||||||
|
|
||||||
let bw = $derived(width / cameraPosition[2]);
|
let bw = $derived(width / cameraPosition[2]);
|
||||||
let bh = $derived(height / cameraPosition[2]);
|
let bh = $derived(height / cameraPosition[2]);
|
||||||
</script>
|
</script>
|
||||||
@@ -48,31 +38,27 @@
|
|||||||
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],
|
||||||
},
|
},
|
||||||
gridType: {
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
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]}
|
||||||
uniforms.gridType.value={gridType}
|
|
||||||
/>
|
/>
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
</T.Group>
|
</T.Group>
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeId, NodeInstance } from '@nodarium/types';
|
import { HTML } from "@threlte/extras";
|
||||||
import { HTML } from '@threlte/extras';
|
import { onMount } from "svelte";
|
||||||
import { onMount } from 'svelte';
|
import type { NodeInstance, NodeId } from "@nodarium/types";
|
||||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paddingLeft?: number;
|
|
||||||
paddingRight?: number;
|
|
||||||
paddingTop?: number;
|
|
||||||
paddingBottom?: number;
|
|
||||||
onnode: (n: NodeInstance) => void;
|
onnode: (n: NodeInstance) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const padding = 10;
|
const { onnode }: Props = $props();
|
||||||
|
|
||||||
const {
|
|
||||||
paddingLeft = padding,
|
|
||||||
paddingRight = padding,
|
|
||||||
paddingTop = padding,
|
|
||||||
paddingBottom = padding,
|
|
||||||
onnode
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const graph = getGraphManager();
|
const graph = getGraphManager();
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
@@ -29,18 +17,15 @@
|
|||||||
let value = $state<string>();
|
let value = $state<string>();
|
||||||
let activeNodeId = $state<NodeId>();
|
let activeNodeId = $state<NodeId>();
|
||||||
|
|
||||||
const MENU_WIDTH = 150;
|
|
||||||
const MENU_HEIGHT = 350;
|
|
||||||
|
|
||||||
const allNodes = graphState.activeSocket
|
const allNodes = graphState.activeSocket
|
||||||
? graph.getPossibleNodes(graphState.activeSocket)
|
? graph.getPossibleNodes(graphState.activeSocket)
|
||||||
: graph.getNodeDefinitions();
|
: graph.getNodeDefinitions();
|
||||||
|
|
||||||
function filterNodes() {
|
function filterNodes() {
|
||||||
return allNodes.filter((node) => node.id.includes(value ?? ''));
|
return allNodes.filter((node) => node.id.includes(value ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = $derived(value === '' ? allNodes : filterNodes());
|
const nodes = $derived(value === "" ? allNodes : filterNodes());
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (nodes) {
|
if (nodes) {
|
||||||
if (activeNodeId === undefined) {
|
if (activeNodeId === undefined) {
|
||||||
@@ -54,38 +39,38 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleNodeCreation(nodeType: NodeInstance['type']) {
|
function handleNodeCreation(nodeType: NodeInstance["type"]) {
|
||||||
if (!graphState.addMenuPosition) return;
|
if (!graphState.addMenuPosition) return;
|
||||||
onnode?.({
|
onnode?.({
|
||||||
id: -1,
|
id: -1,
|
||||||
type: nodeType,
|
type: nodeType,
|
||||||
position: [...graphState.addMenuPosition],
|
position: [...graphState.addMenuPosition],
|
||||||
props: {},
|
props: {},
|
||||||
state: {}
|
state: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
if (event.key === "Escape") {
|
||||||
graphState.addMenuPosition = null;
|
graphState.addMenuPosition = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === "ArrowDown") {
|
||||||
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
||||||
activeNodeId = nodes[(index + 1) % nodes.length].id;
|
activeNodeId = nodes[(index + 1) % nodes.length].id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === "ArrowUp") {
|
||||||
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
||||||
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
|
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
if (activeNodeId && graphState.addMenuPosition) {
|
if (activeNodeId && graphState.addMenuPosition) {
|
||||||
handleNodeCreation(activeNodeId);
|
handleNodeCreation(activeNodeId);
|
||||||
}
|
}
|
||||||
@@ -93,49 +78,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampAddMenuPosition() {
|
|
||||||
if (!graphState.addMenuPosition) return;
|
|
||||||
|
|
||||||
const camX = graphState.cameraPosition[0];
|
|
||||||
const camY = graphState.cameraPosition[1];
|
|
||||||
const zoom = graphState.cameraPosition[2];
|
|
||||||
|
|
||||||
const halfViewportWidth = (graphState.width / 2) / zoom;
|
|
||||||
const halfViewportHeight = (graphState.height / 2) / zoom;
|
|
||||||
|
|
||||||
const halfMenuWidth = (MENU_WIDTH / 2) / zoom;
|
|
||||||
const halfMenuHeight = (MENU_HEIGHT / 2) / zoom;
|
|
||||||
|
|
||||||
const minX = camX - halfViewportWidth - halfMenuWidth + paddingLeft / zoom;
|
|
||||||
const maxX = camX + halfViewportWidth - halfMenuWidth - paddingRight / zoom;
|
|
||||||
const minY = camY - halfViewportHeight - halfMenuHeight + paddingTop / zoom;
|
|
||||||
const maxY = camY + halfViewportHeight - halfMenuHeight - paddingBottom / zoom;
|
|
||||||
|
|
||||||
const clampedX = Math.max(
|
|
||||||
minX + halfMenuWidth,
|
|
||||||
Math.min(graphState.addMenuPosition[0], maxX - halfMenuWidth)
|
|
||||||
);
|
|
||||||
const clampedY = Math.max(
|
|
||||||
minY + halfMenuHeight,
|
|
||||||
Math.min(graphState.addMenuPosition[1], maxY - halfMenuHeight)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clampedX !== graphState.addMenuPosition[0] || clampedY !== graphState.addMenuPosition[1]) {
|
|
||||||
graphState.addMenuPosition = [clampedX, clampedY];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const pos = graphState.addMenuPosition;
|
|
||||||
const zoom = graphState.cameraPosition[2];
|
|
||||||
const width = graphState.width;
|
|
||||||
const height = graphState.height;
|
|
||||||
|
|
||||||
if (pos && zoom && width && height) {
|
|
||||||
clampAddMenuPosition();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
input.disabled = false;
|
input.disabled = false;
|
||||||
setTimeout(() => input.focus(), 50);
|
setTimeout(() => input.focus(), 50);
|
||||||
@@ -163,14 +105,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#each nodes as node (node.id)}
|
{#each nodes as node}
|
||||||
<div
|
<div
|
||||||
class="result"
|
class="result"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-selected={node.id === activeNodeId}
|
aria-selected={node.id === activeNodeId}
|
||||||
onkeydown={(event) => {
|
onkeydown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
handleNodeCreation(node.id);
|
handleNodeCreation(node.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -183,7 +125,7 @@
|
|||||||
activeNodeId = node.id;
|
activeNodeId = node.id;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{node.id.split('/').at(-1)}
|
{node.id.split("/").at(-1)}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +138,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background: var(--color-layer-0);
|
background: var(--layer-0);
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -215,30 +157,28 @@
|
|||||||
|
|
||||||
.add-menu-wrapper {
|
.add-menu-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--color-layer-1);
|
background: var(--layer-1);
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: solid 2px var(--color-layer-2);
|
border: solid 2px var(--layer-2);
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
min-height: none;
|
min-height: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result {
|
.result {
|
||||||
padding: 1em 0.9em;
|
padding: 1em 0.9em;
|
||||||
border-bottom: solid 1px var(--color-layer-2);
|
border-bottom: solid 1px var(--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(--color-layer-2);
|
background: var(--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,15 +24,14 @@
|
|||||||
<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(--color-outline);
|
border: solid 2px var(--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(--color-layer-1);
|
background: var(--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(--color-outline);
|
border: 1px solid var(--outline);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Box } from '@nodarium/types';
|
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||||
import { T } from '@threlte/core';
|
import { points, lines } from './store.js';
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
import { T } from '@threlte/core';
|
||||||
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 (getEachKey(point))}
|
{#each $points as point}
|
||||||
<T.Mesh
|
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}>
|
||||||
position.x={point.x}
|
<T.CircleGeometry args={[0.2, 32]} />
|
||||||
position.y={point.y}
|
<T.MeshBasicMaterial color="red" />
|
||||||
position.z={point.z}
|
</T.Mesh>
|
||||||
rotation.x={-Math.PI / 2}
|
|
||||||
>
|
|
||||||
<T.CircleGeometry args={[0.2, 32]} />
|
|
||||||
<T.MeshBasicMaterial color="red" />
|
|
||||||
</T.Mesh>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each $rects as rect, i (getEachKey(rect))}
|
{#each $lines as line}
|
||||||
<T.Mesh
|
<T.Mesh>
|
||||||
position.x={(rect.minX + rect.maxX) / 2}
|
<MeshLineGeometry points={line} />
|
||||||
position.y={0}
|
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
||||||
position.z={(rect.minY + rect.maxY) / 2}
|
</T.Mesh>
|
||||||
rotation.x={-Math.PI / 2}
|
|
||||||
>
|
|
||||||
<T.PlaneGeometry args={[rect.maxX - rect.minX, rect.maxY - rect.minY]} />
|
|
||||||
<T.MeshBasicMaterial
|
|
||||||
color={new Color().setHSL((i * 1.77) % 1, 1, 0.5)}
|
|
||||||
opacity={0.9}
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#each $lines as line (getEachKey(line))}
|
|
||||||
<T.Mesh position.y={1}>
|
|
||||||
<MeshLineGeometry points={line.points} />
|
|
||||||
<MeshLineMaterial
|
|
||||||
color={line.color || 'red'}
|
|
||||||
linewidth={1}
|
|
||||||
attenuate={false}
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { Box } from '@nodarium/types';
|
import { Vector3 } from "three/src/math/Vector3.js";
|
||||||
import type { Color } from 'three';
|
import { lines, points } from "./store";
|
||||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
|
||||||
import Component from './Debug.svelte';
|
|
||||||
import { lines, points, rects } from './store';
|
|
||||||
|
|
||||||
export function debugPosition(x: number, y: number) {
|
export function debugPosition(x: number, y: number) {
|
||||||
points.update((p) => {
|
points.update((p) => {
|
||||||
@@ -11,27 +8,18 @@ export function debugPosition(x: number, y: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debugRect(rect: Box) {
|
|
||||||
rects.update((r) => {
|
|
||||||
r.push(rect);
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear() {
|
export function clear() {
|
||||||
points.set([]);
|
points.set([]);
|
||||||
lines.set([]);
|
lines.set([]);
|
||||||
rects.set([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debugLine(points: Vector3[], color?: Color) {
|
export function debugLine(line: Vector3[]) {
|
||||||
lines.update((l) => {
|
lines.update((l) => {
|
||||||
l.push({ points, color });
|
l.push(line);
|
||||||
return l;
|
return l;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Component;
|
import Component from "./Debug.svelte";
|
||||||
export function clearLines() {
|
|
||||||
lines.set([]);
|
export default Component
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { Box } from '@nodarium/types';
|
import { writable } from "svelte/store";
|
||||||
import { writable } from 'svelte/store';
|
import { Vector3 } from "three/src/math/Vector3.js";
|
||||||
import type { Color } from 'three';
|
|
||||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
|
||||||
|
|
||||||
export const points = writable<Vector3[]>([]);
|
export const points = writable<Vector3[]>([]);
|
||||||
export const rects = writable<Box[]>([]);
|
|
||||||
export const lines = writable<{ points: Vector3[]; color?: Color }[]>([]);
|
export const lines = writable<Vector3[][]>([]);
|
||||||
|
|||||||
@@ -1,17 +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.outline.clone(),
|
color: colors.edge.clone(),
|
||||||
toneMapped: false
|
toneMapped: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (appSettings.value.theme === undefined) {
|
appSettings.value.theme;
|
||||||
return;
|
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
|
||||||
}
|
lineColor = colors.edge.clone().convertSRGBToLinear();
|
||||||
circleMaterial.color = colors.outline.clone().convertSRGBToLinear();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,22 +20,17 @@
|
|||||||
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 { appSettings } from '$lib/settings/app-settings.svelte';
|
import { T } from "@threlte/core";
|
||||||
import { T } from '@threlte/core';
|
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
import { MeshBasicMaterial, Vector3 } from "three";
|
||||||
import { onDestroy } from 'svelte';
|
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
|
||||||
import { MeshBasicMaterial, Vector3 } from 'three';
|
import { Vector2 } from "three/src/math/Vector2.js";
|
||||||
import { CubicBezierCurve } from 'three/src/extras/curves/CubicBezierCurve.js';
|
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||||
import { Vector2 } from 'three/src/math/Vector2.js';
|
|
||||||
import { getGraphState } from '../graph-state.svelte';
|
|
||||||
import MeshGradientLineMaterial from './MeshGradientLine/MeshGradientLineMaterial.svelte';
|
|
||||||
|
|
||||||
const graphState = getGraphState();
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
x1: number;
|
x1: number;
|
||||||
@@ -42,33 +38,27 @@
|
|||||||
x2: number;
|
x2: number;
|
||||||
y2: number;
|
y2: number;
|
||||||
z: number;
|
z: number;
|
||||||
id?: string;
|
|
||||||
inputType?: string;
|
|
||||||
outputType?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { x1, y1, x2, y2, z, inputType = 'unknown', outputType = 'unknown', id }: Props = $props();
|
const { x1, y1, x2, y2, z }: Props = $props();
|
||||||
|
|
||||||
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
|
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
|
||||||
|
|
||||||
const inputColor = $derived(graphState.colors.getColor(inputType));
|
|
||||||
const outputColor = $derived(graphState.colors.getColor(outputType));
|
|
||||||
|
|
||||||
let points = $state<Vector3[]>([]);
|
let points = $state<Vector3[]>([]);
|
||||||
|
|
||||||
let lastId: string | null = null;
|
let lastId: string | null = null;
|
||||||
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
|
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const new_x = x2 - x1;
|
const new_x = x2 - x1;
|
||||||
const new_y = y2 - y1;
|
const new_y = y2 - y1;
|
||||||
|
const curveId = `${x1}-${y1}-${x2}-${y2}`;
|
||||||
if (lastId === curveId) {
|
if (lastId === curveId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -82,15 +72,6 @@
|
|||||||
.getPoints(samples)
|
.getPoints(samples)
|
||||||
.map((p) => new Vector3(p.x, 0, p.y))
|
.map((p) => new Vector3(p.x, 0, p.y))
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
if (id) {
|
|
||||||
graphState.setEdgeGeometry(
|
|
||||||
id,
|
|
||||||
x1,
|
|
||||||
y1,
|
|
||||||
$state.snapshot(points) as unknown as Vector3[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -98,10 +79,6 @@
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (id) graphState.removeEdgeGeometry(id);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
@@ -109,9 +86,9 @@
|
|||||||
position.z={y1}
|
position.z={y1}
|
||||||
position.y={0.8}
|
position.y={0.8}
|
||||||
rotation.x={-Math.PI / 2}
|
rotation.x={-Math.PI / 2}
|
||||||
|
material={circleMaterial}
|
||||||
>
|
>
|
||||||
<T.CircleGeometry args={[0.5, 16]} />
|
<T.CircleGeometry args={[0.5, 16]} />
|
||||||
<T.MeshBasicMaterial color={inputColor} toneMapped={false} />
|
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
@@ -122,28 +99,9 @@
|
|||||||
material={circleMaterial}
|
material={circleMaterial}
|
||||||
>
|
>
|
||||||
<T.CircleGeometry args={[0.5, 16]} />
|
<T.CircleGeometry args={[0.5, 16]} />
|
||||||
<T.MeshBasicMaterial color={outputColor} toneMapped={false} />
|
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|
||||||
{#if graphState.hoveredEdgeId === id}
|
|
||||||
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
|
||||||
<MeshLineGeometry {points} />
|
|
||||||
<MeshLineMaterial
|
|
||||||
width={thickness * 5}
|
|
||||||
color={inputColor}
|
|
||||||
tonemapped={false}
|
|
||||||
opacity={0.5}
|
|
||||||
transparent
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
||||||
<MeshLineGeometry {points} />
|
<MeshLineGeometry {points} />
|
||||||
<MeshGradientLineMaterial
|
<MeshLineMaterial width={thickness} color={lineColor} />
|
||||||
width={thickness}
|
|
||||||
colorStart={inputColor}
|
|
||||||
colorEnd={outputColor}
|
|
||||||
tonemapped={false}
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { T, useThrelte } from '@threlte/core';
|
|
||||||
import { Color, ShaderMaterial, Vector2 } from 'three';
|
|
||||||
import fragmentShader from './fragment.frag';
|
|
||||||
import type { MeshLineMaterialProps } from './types';
|
|
||||||
import vertexShader from './vertex.vert';
|
|
||||||
|
|
||||||
let {
|
|
||||||
opacity = 1,
|
|
||||||
colorStart = '#ffffff',
|
|
||||||
colorEnd = '#ffffff',
|
|
||||||
dashOffset = 0,
|
|
||||||
dashArray = 0,
|
|
||||||
dashRatio = 0,
|
|
||||||
attenuate = true,
|
|
||||||
width = 1,
|
|
||||||
scaleDown = 0,
|
|
||||||
alphaMap,
|
|
||||||
ref = $bindable(),
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: MeshLineMaterialProps = $props();
|
|
||||||
|
|
||||||
let { invalidate, size } = useThrelte();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const uniforms = {
|
|
||||||
lineWidth: { value: width },
|
|
||||||
colorStart: { value: new Color(colorStart) },
|
|
||||||
colorEnd: { value: new Color(colorEnd) },
|
|
||||||
opacity: { value: opacity },
|
|
||||||
resolution: { value: new Vector2(1, 1) },
|
|
||||||
sizeAttenuation: { value: attenuate ? 1 : 0 },
|
|
||||||
dashArray: { value: dashArray },
|
|
||||||
useDash: { value: dashArray > 0 ? 1 : 0 },
|
|
||||||
dashOffset: { value: dashOffset },
|
|
||||||
dashRatio: { value: dashRatio },
|
|
||||||
scaleDown: { value: scaleDown / 10 },
|
|
||||||
alphaTest: { value: 0 },
|
|
||||||
alphaMap: { value: alphaMap },
|
|
||||||
useAlphaMap: { value: alphaMap ? 1 : 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
const material = new ShaderMaterial({ uniforms });
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.lineWidth.value = width;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.opacity.value = opacity;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.resolution.value.set($size.width, $size.height);
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.sizeAttenuation.value = attenuate ? 1 : 0;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.dashArray.value = dashArray;
|
|
||||||
uniforms.useDash.value = dashArray > 0 ? 1 : 0;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.dashOffset.value = dashOffset;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.dashRatio.value = dashRatio;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.scaleDown.value = scaleDown / 10;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.alphaMap.value = alphaMap;
|
|
||||||
uniforms.useAlphaMap.value = alphaMap ? 1 : 0;
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.colorStart.value.set(colorStart);
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect.pre(() => {
|
|
||||||
uniforms.colorEnd.value.set(colorEnd);
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<T
|
|
||||||
is={material}
|
|
||||||
bind:ref
|
|
||||||
{fragmentShader}
|
|
||||||
{vertexShader}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{@render children?.({ ref: material })}
|
|
||||||
</T>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
uniform vec3 colorStart;
|
|
||||||
uniform vec3 colorEnd;
|
|
||||||
|
|
||||||
uniform float useDash;
|
|
||||||
uniform float dashArray;
|
|
||||||
uniform float dashOffset;
|
|
||||||
uniform float dashRatio;
|
|
||||||
uniform sampler2D alphaMap;
|
|
||||||
uniform float useAlphaMap;
|
|
||||||
|
|
||||||
varying vec2 vUV;
|
|
||||||
varying vec4 vColor;
|
|
||||||
varying float vCounters;
|
|
||||||
|
|
||||||
vec4 CustomLinearTosRGB( in vec4 value ) {
|
|
||||||
return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
|
|
||||||
vec4 c = mix(vec4(colorStart,1.0),vec4(colorEnd, 1.0), vCounters);
|
|
||||||
|
|
||||||
if( useAlphaMap == 1. ) c.a *= texture2D( alphaMap, vUV ).r;
|
|
||||||
|
|
||||||
if( useDash == 1. ){
|
|
||||||
c.a *= ceil(mod(vCounters + dashOffset, dashArray) - (dashArray * dashRatio));
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_FragColor = CustomLinearTosRGB(c);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import type { Props } from '@threlte/core';
|
|
||||||
import type { BufferGeometry, Vector3 } from 'three';
|
|
||||||
import type { ColorRepresentation, ShaderMaterial, Texture } from 'three';
|
|
||||||
|
|
||||||
export type MeshLineGeometryProps = Props<BufferGeometry> & {
|
|
||||||
/**
|
|
||||||
* @default []
|
|
||||||
*/
|
|
||||||
points: Vector3[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default 'none'
|
|
||||||
*/
|
|
||||||
shape?: 'none' | 'taper' | 'custom';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default () => 1
|
|
||||||
*/
|
|
||||||
shapeFunction?: (p: number) => number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MeshLineMaterialProps =
|
|
||||||
& Omit<
|
|
||||||
Props<ShaderMaterial>,
|
|
||||||
'uniforms' | 'fragmentShader' | 'vertexShader'
|
|
||||||
>
|
|
||||||
& {
|
|
||||||
/**
|
|
||||||
* @default 1
|
|
||||||
*/
|
|
||||||
opacity?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default '#ffffff'
|
|
||||||
*/
|
|
||||||
color?: ColorRepresentation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
dashOffset?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
dashArray?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
dashRatio?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
attenuate?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default 1
|
|
||||||
*/
|
|
||||||
width?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
scaleDown?: number;
|
|
||||||
alphaMap?: Texture | undefined;
|
|
||||||
};
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
attribute vec3 previous;
|
|
||||||
attribute vec3 next;
|
|
||||||
attribute float side;
|
|
||||||
attribute float width;
|
|
||||||
attribute float counters;
|
|
||||||
|
|
||||||
uniform vec2 resolution;
|
|
||||||
uniform float lineWidth;
|
|
||||||
uniform vec3 color;
|
|
||||||
uniform float opacity;
|
|
||||||
uniform float sizeAttenuation;
|
|
||||||
uniform float scaleDown;
|
|
||||||
|
|
||||||
varying vec2 vUV;
|
|
||||||
varying vec4 vColor;
|
|
||||||
varying float vCounters;
|
|
||||||
|
|
||||||
vec2 intoScreen(vec4 i) {
|
|
||||||
return resolution * (0.5 * i.xy / i.w + 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float aspect = resolution.y / resolution.x;
|
|
||||||
|
|
||||||
mat4 m = projectionMatrix * modelViewMatrix;
|
|
||||||
|
|
||||||
vec4 currentClip = m * vec4( position, 1.0 );
|
|
||||||
vec4 prevClip = m * vec4( previous, 1.0 );
|
|
||||||
vec4 nextClip = m * vec4( next, 1.0 );
|
|
||||||
|
|
||||||
vec4 currentNormed = currentClip / currentClip.w;
|
|
||||||
vec4 prevNormed = prevClip / prevClip.w;
|
|
||||||
vec4 nextNormed = nextClip / nextClip.w;
|
|
||||||
|
|
||||||
vec2 currentScreen = intoScreen(currentNormed);
|
|
||||||
vec2 prevScreen = intoScreen(prevNormed);
|
|
||||||
vec2 nextScreen = intoScreen(nextNormed);
|
|
||||||
|
|
||||||
float actualWidth = lineWidth * width;
|
|
||||||
|
|
||||||
vec2 dir;
|
|
||||||
if(nextScreen == currentScreen) {
|
|
||||||
dir = normalize( currentScreen - prevScreen );
|
|
||||||
} else if(prevScreen == currentScreen) {
|
|
||||||
dir = normalize( nextScreen - currentScreen );
|
|
||||||
} else {
|
|
||||||
vec2 inDir = currentScreen - prevScreen;
|
|
||||||
vec2 outDir = nextScreen - currentScreen;
|
|
||||||
vec2 fullDir = nextScreen - prevScreen;
|
|
||||||
|
|
||||||
if(length(fullDir) > 0.0) {
|
|
||||||
dir = normalize(fullDir);
|
|
||||||
} else if(length(inDir) > 0.0){
|
|
||||||
dir = normalize(inDir);
|
|
||||||
} else {
|
|
||||||
dir = normalize(outDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 normal = vec2(-dir.y, dir.x);
|
|
||||||
|
|
||||||
if(sizeAttenuation != 0.0) {
|
|
||||||
normal /= currentClip.w;
|
|
||||||
normal *= min(resolution.x, resolution.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scaleDown > 0.0) {
|
|
||||||
float dist = length(nextNormed - prevNormed);
|
|
||||||
normal *= smoothstep(0.0, scaleDown, dist);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 offsetInScreen = actualWidth * normal * side * 0.5;
|
|
||||||
|
|
||||||
vec2 withOffsetScreen = currentScreen + offsetInScreen;
|
|
||||||
vec3 withOffsetNormed = vec3((2.0 * withOffsetScreen/resolution - 1.0), currentNormed.z);
|
|
||||||
|
|
||||||
vCounters = counters;
|
|
||||||
vColor = vec4( color, opacity );
|
|
||||||
vUV = uv;
|
|
||||||
|
|
||||||
gl_Position = currentClip.w * vec4(withOffsetNormed, 1.0);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
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,70 +1,68 @@
|
|||||||
import throttle from '$lib/helpers/throttle';
|
|
||||||
import { RemoteNodeRegistry } from '$lib/node-registry/index';
|
|
||||||
import type {
|
import type {
|
||||||
Edge,
|
Edge,
|
||||||
Graph,
|
Graph,
|
||||||
NodeDefinition,
|
|
||||||
NodeId,
|
|
||||||
NodeInput,
|
|
||||||
NodeInstance,
|
NodeInstance,
|
||||||
|
NodeDefinition,
|
||||||
|
NodeInput,
|
||||||
NodeRegistry,
|
NodeRegistry,
|
||||||
Socket
|
NodeId,
|
||||||
} from '@nodarium/types';
|
Socket,
|
||||||
import { fastHashString } from '@nodarium/utils';
|
} from "@nodarium/types";
|
||||||
import { createLogger } from '@nodarium/utils';
|
import { fastHashString } from "@nodarium/utils";
|
||||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteMap } from "svelte/reactivity";
|
||||||
import EventEmitter from './helpers/EventEmitter';
|
import EventEmitter from "./helpers/EventEmitter";
|
||||||
import { HistoryManager } from './history-manager';
|
import { createLogger } from "@nodarium/utils";
|
||||||
|
import throttle from "$lib/helpers/throttle";
|
||||||
|
import { HistoryManager } from "./history-manager";
|
||||||
|
|
||||||
const logger = createLogger('graph-manager');
|
const logger = createLogger("graph-manager");
|
||||||
logger.mute();
|
logger.mute();
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
function areSocketsCompatible(
|
function areSocketsCompatible(
|
||||||
output: string | undefined,
|
output: string | undefined,
|
||||||
inputs: string | (string | undefined)[] | undefined
|
inputs: string | (string | undefined)[] | undefined,
|
||||||
) {
|
) {
|
||||||
if (output === '*') return true;
|
|
||||||
if (Array.isArray(inputs) && output) {
|
if (Array.isArray(inputs) && output) {
|
||||||
return inputs.includes('*') || inputs.includes(output);
|
return inputs.includes(output);
|
||||||
}
|
}
|
||||||
return inputs === output;
|
return inputs === output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
||||||
|
|
||||||
if (firstEdge[0].id !== secondEdge[0].id) {
|
if (firstEdge[0].id !== secondEdge[0].id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstEdge[1] !== secondEdge[1]) {
|
if (firstEdge[1] !== secondEdge[1]) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstEdge[2].id !== secondEdge[2].id) {
|
if (firstEdge[2].id !== secondEdge[2].id) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstEdge[3] !== secondEdge[3]) {
|
if (firstEdge[3] !== secondEdge[3]) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GraphManager extends EventEmitter<{
|
export class GraphManager extends EventEmitter<{
|
||||||
save: Graph;
|
save: Graph;
|
||||||
result: unknown;
|
result: any;
|
||||||
settings: {
|
settings: {
|
||||||
types: Record<string, NodeInput>;
|
types: Record<string, NodeInput>;
|
||||||
values: Record<string, unknown>;
|
values: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
status = $state<'loading' | 'idle' | 'error'>();
|
status = $state<"loading" | "idle" | "error">();
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
graph: Graph = { id: 0, nodes: [], edges: [] };
|
graph: Graph = { id: 0, nodes: [], edges: [] };
|
||||||
@@ -80,7 +78,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
currentUndoGroup: number | null = null;
|
currentUndoGroup: number | null = null;
|
||||||
|
|
||||||
inputSockets = $derived.by(() => {
|
inputSockets = $derived.by(() => {
|
||||||
const s = new SvelteSet<string>();
|
const s = new Set<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]}`);
|
||||||
}
|
}
|
||||||
@@ -90,7 +88,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
history: HistoryManager = new HistoryManager();
|
history: HistoryManager = new HistoryManager();
|
||||||
execute = throttle(() => {
|
execute = throttle(() => {
|
||||||
if (this.loaded === false) return;
|
if (this.loaded === false) return;
|
||||||
this.emit('result', this.serialize());
|
this.emit("result", this.serialize());
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
constructor(public registry: NodeRegistry) {
|
constructor(public registry: NodeRegistry) {
|
||||||
@@ -102,28 +100,27 @@ export class GraphManager extends EventEmitter<{
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
position: [...node.position],
|
position: [...node.position],
|
||||||
type: node.type,
|
type: node.type,
|
||||||
props: node.props
|
props: node.props,
|
||||||
})) as NodeInstance[];
|
})) as NodeInstance[];
|
||||||
const edges = this.edges.map((edge) => [
|
const edges = this.edges.map((edge) => [
|
||||||
edge[0].id,
|
edge[0].id,
|
||||||
edge[1],
|
edge[1],
|
||||||
edge[2].id,
|
edge[2].id,
|
||||||
edge[3]
|
edge[3],
|
||||||
]) as Graph['edges'];
|
]) as Graph["edges"];
|
||||||
const serialized = {
|
const serialized = {
|
||||||
id: this.graph.id,
|
id: this.graph.id,
|
||||||
settings: $state.snapshot(this.settings),
|
settings: $state.snapshot(this.settings),
|
||||||
meta: $state.snapshot(this.graph.meta),
|
|
||||||
nodes,
|
nodes,
|
||||||
edges
|
edges,
|
||||||
};
|
};
|
||||||
logger.log('serializing graph', serialized);
|
logger.log("serializing graph", serialized);
|
||||||
return clone($state.snapshot(serialized));
|
return clone($state.snapshot(serialized));
|
||||||
}
|
}
|
||||||
|
|
||||||
private lastSettingsHash = 0;
|
private lastSettingsHash = 0;
|
||||||
setSettings(settings: Record<string, unknown>) {
|
setSettings(settings: Record<string, unknown>) {
|
||||||
const hash = fastHashString(JSON.stringify(settings));
|
let hash = fastHashString(JSON.stringify(settings));
|
||||||
if (hash === this.lastSettingsHash) return;
|
if (hash === this.lastSettingsHash) return;
|
||||||
this.lastSettingsHash = hash;
|
this.lastSettingsHash = hash;
|
||||||
|
|
||||||
@@ -137,7 +134,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLinkedNodes(node: NodeInstance) {
|
getLinkedNodes(node: NodeInstance) {
|
||||||
const nodes = new SvelteSet<NodeInstance>();
|
const nodes = new Set<NodeInstance>();
|
||||||
const stack = [node];
|
const stack = [node];
|
||||||
while (stack.length) {
|
while (stack.length) {
|
||||||
const n = stack.pop();
|
const n = stack.pop();
|
||||||
@@ -151,97 +148,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return [...nodes.values()];
|
return [...nodes.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
getEdgeId(e: Edge) {
|
|
||||||
return `${e[0].id}-${e[1]}-${e[2].id}-${e[3]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdgeById(id: string): Edge | undefined {
|
|
||||||
return this.edges.find((e) => this.getEdgeId(e) === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
dropNodeOnEdge(nodeId: number, edge: Edge) {
|
|
||||||
const draggedNode = this.getNode(nodeId);
|
|
||||||
if (!draggedNode || !draggedNode.state?.type) return;
|
|
||||||
|
|
||||||
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
|
||||||
|
|
||||||
const draggedInputs = Object.entries(draggedNode.state.type.inputs ?? {});
|
|
||||||
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
|
||||||
|
|
||||||
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
|
|
||||||
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
|
||||||
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
|
||||||
|
|
||||||
const bestInputEntry = draggedInputs.find(([, input]) => {
|
|
||||||
const accepted = [input.type, ...(input.accepts || [])];
|
|
||||||
return areSocketsCompatible(edgeOutputSocketType, accepted);
|
|
||||||
});
|
|
||||||
|
|
||||||
const bestOutputIdx = draggedOutputs.findIndex(outputType =>
|
|
||||||
areSocketsCompatible(outputType, targetAcceptedTypes)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!bestInputEntry || bestOutputIdx === -1) {
|
|
||||||
logger.error('Could not find compatible sockets for drop');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startUndoGroup();
|
|
||||||
|
|
||||||
this.removeEdge(edge, { applyDeletion: false });
|
|
||||||
|
|
||||||
this.createEdge(fromNode, fromSocketIdx, draggedNode, bestInputEntry[0], {
|
|
||||||
applyUpdate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
this.createEdge(draggedNode, bestOutputIdx, toNode, toSocketKey, {
|
|
||||||
applyUpdate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
this.saveUndoGroup();
|
|
||||||
this.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
getPossibleDropOnEdges(nodeId: number): Edge[] {
|
|
||||||
const draggedNode = this.getNode(nodeId);
|
|
||||||
if (!draggedNode || !draggedNode.state?.type) return [];
|
|
||||||
|
|
||||||
const draggedInputs = Object.values(draggedNode.state.type.inputs ?? {});
|
|
||||||
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
|
||||||
|
|
||||||
// Optimization: Pre-calculate parents to avoid cycles
|
|
||||||
const parentIds = new SvelteSet(this.getParentsOfNode(draggedNode).map(n => n.id));
|
|
||||||
|
|
||||||
return this.edges.filter((edge) => {
|
|
||||||
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
|
||||||
|
|
||||||
// 1. Prevent cycles: If the target node is already a parent, we can't drop here
|
|
||||||
if (parentIds.has(toNode.id)) return false;
|
|
||||||
|
|
||||||
// 2. Prevent self-dropping: Don't drop on edges already connected to this node
|
|
||||||
if (fromNode.id === nodeId || toNode.id === nodeId) return false;
|
|
||||||
|
|
||||||
// 3. Check if edge.source can plug into ANY draggedNode.input
|
|
||||||
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
|
|
||||||
const canPlugIntoDragged = draggedInputs.some(input => {
|
|
||||||
const acceptedTypes = [input.type, ...(input.accepts || [])];
|
|
||||||
return areSocketsCompatible(edgeOutputSocketType, acceptedTypes);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canPlugIntoDragged) return false;
|
|
||||||
|
|
||||||
// 4. Check if ANY draggedNode.output can plug into edge.target
|
|
||||||
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
|
||||||
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
|
||||||
|
|
||||||
const draggedCanPlugIntoTarget = draggedOutputs.some(outputType =>
|
|
||||||
areSocketsCompatible(outputType, targetAcceptedTypes)
|
|
||||||
);
|
|
||||||
|
|
||||||
return draggedCanPlugIntoTarget;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
|
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
|
||||||
const edges = [];
|
const edges = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -249,14 +155,14 @@ export class GraphManager extends EventEmitter<{
|
|||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (nodes.includes(child)) {
|
if (nodes.includes(child)) {
|
||||||
const edge = this.edges.find(
|
const edge = this.edges.find(
|
||||||
(e) => e[0].id === node.id && e[2].id === child.id
|
(e) => e[0].id === node.id && e[2].id === child.id,
|
||||||
);
|
);
|
||||||
if (edge) {
|
if (edge) {
|
||||||
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
|
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
string
|
string,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,24 +173,24 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _init(graph: Graph) {
|
private _init(graph: Graph) {
|
||||||
const nodes = new SvelteMap(
|
const nodes = new Map(
|
||||||
graph.nodes.map((node) => {
|
graph.nodes.map((node) => {
|
||||||
const nodeType = this.registry.getNode(node.type);
|
const nodeType = this.registry.getNode(node.type);
|
||||||
const n = node as NodeInstance;
|
const n = node as NodeInstance;
|
||||||
if (nodeType) {
|
if (nodeType) {
|
||||||
n.state = {
|
n.state = {
|
||||||
type: nodeType
|
type: nodeType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return [node.id, n];
|
return [node.id, n];
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.edges = graph.edges.map((edge) => {
|
const edges = graph.edges.map((edge) => {
|
||||||
const from = nodes.get(edge[0]);
|
const from = nodes.get(edge[0]);
|
||||||
const to = nodes.get(edge[2]);
|
const to = nodes.get(edge[2]);
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
throw new Error('Edge references non-existing node');
|
throw new Error("Edge references non-existing node");
|
||||||
}
|
}
|
||||||
from.state.children = from.state.children || [];
|
from.state.children = from.state.children || [];
|
||||||
from.state.children.push(to);
|
from.state.children.push(to);
|
||||||
@@ -293,6 +199,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return [from, edge[1], to, edge[3]] as Edge;
|
return [from, edge[1], to, edge[3]] as Edge;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.edges = [...edges];
|
||||||
|
|
||||||
this.nodes.clear();
|
this.nodes.clear();
|
||||||
for (const [id, node] of nodes) {
|
for (const [id, node] of nodes) {
|
||||||
this.nodes.set(id, node);
|
this.nodes.set(id, node);
|
||||||
@@ -306,36 +214,21 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.graph = graph;
|
this.graph = graph;
|
||||||
this.status = 'loading';
|
this.status = "loading";
|
||||||
this.id = graph.id;
|
this.id = graph.id;
|
||||||
|
|
||||||
logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id });
|
logger.info("loading graph", $state.snapshot(graph));
|
||||||
|
|
||||||
const nodeIds = Array.from(new SvelteSet([...graph.nodes.map((n) => n.type)]));
|
const nodeIds = Array.from(new Set([...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
|
logger.info("loaded node types", this.registry.getAllNodes());
|
||||||
const allCollections = new SvelteSet<`${string}/${string}`>();
|
|
||||||
for (const id of nodeIds) {
|
|
||||||
const [user, collection] = id.split('/');
|
|
||||||
allCollections.add(`${user}/${collection}`);
|
|
||||||
}
|
|
||||||
for (const collection of allCollections) {
|
|
||||||
remoteRegistry
|
|
||||||
.fetchCollection(collection)
|
|
||||||
.then((collection: { nodes: { id: NodeId }[] }) => {
|
|
||||||
const ids = collection.nodes.map((n) => n.id);
|
|
||||||
return this.registry.load(ids);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('loaded node types', this.registry.getAllNodes());
|
|
||||||
|
|
||||||
for (const node of this.graph.nodes) {
|
for (const node of this.graph.nodes) {
|
||||||
const nodeType = this.registry.getNode(node.type);
|
const nodeType = this.registry.getNode(node.type);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
logger.error(`Node type not found: ${node.type}`);
|
logger.error(`Node type not found: ${node.type}`);
|
||||||
this.status = 'error';
|
this.status = "error";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Turn into runtime node
|
// Turn into runtime node
|
||||||
@@ -355,16 +248,16 @@ 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) {
|
||||||
const settingId = type.inputs[key].setting;
|
let settingId = type.inputs[key].setting;
|
||||||
if (settingId) {
|
if (settingId) {
|
||||||
settingTypes[settingId] = {
|
settingTypes[settingId] = {
|
||||||
__node_type: type.id,
|
__node_type: type.id,
|
||||||
__node_input: key,
|
__node_input: key,
|
||||||
...type.inputs[key]
|
...type.inputs[key],
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
settingValues[settingId] === undefined
|
settingValues[settingId] === undefined &&
|
||||||
&& 'value' in type.inputs[key]
|
"value" in type.inputs[key]
|
||||||
) {
|
) {
|
||||||
settingValues[settingId] = type.inputs[key].value;
|
settingValues[settingId] = type.inputs[key].value;
|
||||||
}
|
}
|
||||||
@@ -374,14 +267,14 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.settings = settingValues;
|
this.settings = settingValues;
|
||||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
this.emit("settings", { types: settingTypes, values: settingValues });
|
||||||
|
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
this._init(this.graph);
|
this._init(this.graph);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
this.status = 'idle';
|
this.status = "idle";
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||||
@@ -410,13 +303,13 @@ 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) {
|
||||||
const settingId = nodeType.inputs[key].setting;
|
let settingId = nodeType.inputs[key].setting;
|
||||||
if (settingId) {
|
if (settingId) {
|
||||||
settingTypes[settingId] = nodeType.inputs[key];
|
settingTypes[settingId] = nodeType.inputs[key];
|
||||||
if (
|
if (
|
||||||
settingValues
|
settingValues &&
|
||||||
&& settingValues?.[settingId] === undefined
|
settingValues?.[settingId] === undefined &&
|
||||||
&& 'value' in nodeType.inputs[key]
|
"value" in nodeType.inputs[key]
|
||||||
) {
|
) {
|
||||||
settingValues[settingId] = nodeType.inputs[key].value;
|
settingValues[settingId] = nodeType.inputs[key].value;
|
||||||
}
|
}
|
||||||
@@ -426,7 +319,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
this.settings = settingValues;
|
this.settings = settingValues;
|
||||||
this.settingTypes = settingTypes;
|
this.settingTypes = settingTypes;
|
||||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
this.emit("settings", { types: settingTypes, values: settingValues });
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildren(node: NodeInstance) {
|
getChildren(node: NodeInstance) {
|
||||||
@@ -475,7 +368,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
|
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
|
||||||
if (outputType === inputType) {
|
if (outputType === inputType) {
|
||||||
this.createEdge(from, fromSocket, to, toSocket, {
|
this.createEdge(from, fromSocket, to, toSocket, {
|
||||||
applyUpdate: false
|
applyUpdate: false,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -508,9 +401,9 @@ 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 SvelteMap<number, number>();
|
const idMap = new Map<number, number>();
|
||||||
|
|
||||||
let startId = this.createNodeId();
|
let startId = this.createNodeId()
|
||||||
|
|
||||||
nodes = nodes.map((node) => {
|
nodes = nodes.map((node) => {
|
||||||
const id = startId++;
|
const id = startId++;
|
||||||
@@ -527,7 +420,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
|
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
|
||||||
|
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
throw new Error('Edge references non-existing node');
|
throw new Error("Edge references non-existing node");
|
||||||
}
|
}
|
||||||
|
|
||||||
to.state.parents = to.state.parents || [];
|
to.state.parents = to.state.parents || [];
|
||||||
@@ -552,11 +445,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
createNode({
|
createNode({
|
||||||
type,
|
type,
|
||||||
position,
|
position,
|
||||||
props = {}
|
props = {},
|
||||||
}: {
|
}: {
|
||||||
type: NodeInstance['type'];
|
type: NodeInstance["type"];
|
||||||
position: NodeInstance['position'];
|
position: NodeInstance["position"];
|
||||||
props: NodeInstance['props'];
|
props: NodeInstance["props"];
|
||||||
}) {
|
}) {
|
||||||
const nodeType = this.registry.getNode(type);
|
const nodeType = this.registry.getNode(type);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
@@ -569,14 +462,14 @@ export class GraphManager extends EventEmitter<{
|
|||||||
type,
|
type,
|
||||||
position,
|
position,
|
||||||
state: { type: nodeType },
|
state: { type: nodeType },
|
||||||
props
|
props,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nodes.set(node.id, node);
|
this.nodes.set(node.id, node);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
return node;
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
createEdge(
|
createEdge(
|
||||||
@@ -584,16 +477,17 @@ export class GraphManager extends EventEmitter<{
|
|||||||
fromSocket: number,
|
fromSocket: number,
|
||||||
to: NodeInstance,
|
to: NodeInstance,
|
||||||
toSocket: string,
|
toSocket: string,
|
||||||
{ applyUpdate = true } = {}
|
{ applyUpdate = true } = {},
|
||||||
): Edge | undefined {
|
): Edge | undefined {
|
||||||
|
|
||||||
const existingEdges = this.getEdgesToNode(to);
|
const existingEdges = this.getEdgesToNode(to);
|
||||||
|
|
||||||
// check if this exact edge already exists
|
// check if this exact edge already exists
|
||||||
const existingEdge = existingEdges.find(
|
const existingEdge = existingEdges.find(
|
||||||
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
|
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
|
||||||
);
|
);
|
||||||
if (existingEdge) {
|
if (existingEdge) {
|
||||||
logger.error('Edge already exists', existingEdge);
|
logger.error("Edge already exists", existingEdge);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,13 +500,13 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
|
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const edgeToBeReplaced = this.edges.find(
|
const edgeToBeReplaced = this.edges.find(
|
||||||
(e) => e[2].id === to.id && e[3] === toSocket
|
(e) => e[2].id === to.id && e[3] === toSocket,
|
||||||
);
|
);
|
||||||
if (edgeToBeReplaced) {
|
if (edgeToBeReplaced) {
|
||||||
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
||||||
@@ -639,7 +533,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const nextState = this.history.undo();
|
const nextState = this.history.undo();
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
this._init(nextState);
|
this._init(nextState);
|
||||||
this.emit('save', this.serialize());
|
this.emit("save", this.serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +541,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const nextState = this.history.redo();
|
const nextState = this.history.redo();
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
this._init(nextState);
|
this._init(nextState);
|
||||||
this.emit('save', this.serialize());
|
this.emit("save", this.serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,15 +558,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
if (this.currentUndoGroup) return;
|
if (this.currentUndoGroup) return;
|
||||||
const state = this.serialize();
|
const state = this.serialize();
|
||||||
this.history.save(state);
|
this.history.save(state);
|
||||||
|
this.emit("save", state);
|
||||||
// This is some stupid race condition where the graph-manager emits a save event
|
logger.log("saving graphs", state);
|
||||||
// when the graph is not fully loaded
|
|
||||||
if (this.nodes.size === 0 && this.edges.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('save', state);
|
|
||||||
logger.log('saving graphs', state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: NodeInstance) {
|
getParentsOfNode(node: NodeInstance) {
|
||||||
@@ -680,7 +567,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const stack = node.state?.parents?.slice(0);
|
const stack = node.state?.parents?.slice(0);
|
||||||
while (stack?.length) {
|
while (stack?.length) {
|
||||||
if (parents.length > 1000000) {
|
if (parents.length > 1000000) {
|
||||||
logger.warn('Infinite loop detected');
|
logger.warn("Infinite loop detected");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const parent = stack.pop();
|
const parent = stack.pop();
|
||||||
@@ -699,28 +586,26 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const definitions = typeof socket.index === 'string'
|
const definitions = typeof socket.index === "string"
|
||||||
? allDefinitions.filter(s => {
|
? allDefinitions.filter(s => {
|
||||||
return s.outputs?.find(_s =>
|
return s.outputs?.find(_s => Object
|
||||||
Object
|
.values(nodeType?.inputs || {})
|
||||||
.values(nodeType?.inputs || {})
|
.map(s => s.type)
|
||||||
.map(s => s.type)
|
.includes(_s as NodeInput["type"])
|
||||||
.includes(_s as NodeInput['type'])
|
)
|
||||||
);
|
|
||||||
})
|
})
|
||||||
: allDefinitions.filter(s =>
|
: allDefinitions.filter(s => Object
|
||||||
Object
|
.values(s.inputs ?? {})
|
||||||
.values(s.inputs ?? {})
|
.find(s => {
|
||||||
.find(s => {
|
if (s.hidden) return false;
|
||||||
if (s.hidden) return false;
|
if (nodeType.outputs?.includes(s.type)) {
|
||||||
if (nodeType.outputs?.includes(s.type)) {
|
return true
|
||||||
return true;
|
}
|
||||||
}
|
return s.accepts?.find(a => nodeType.outputs?.includes(a))
|
||||||
return s.accepts?.find(a => nodeType.outputs?.includes(a));
|
}))
|
||||||
})
|
|
||||||
);
|
return definitions
|
||||||
|
|
||||||
return definitions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
||||||
@@ -730,11 +615,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
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 SvelteSet(this.getChildren(node).map((n) => n.id));
|
const children = new Set(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),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ownType = nodeType?.inputs?.[index].type;
|
const ownType = nodeType?.inputs?.[index].type;
|
||||||
@@ -749,25 +634,21 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof index === 'number') {
|
} else if (typeof index === "number") {
|
||||||
// if index is a number, we are an output looking for inputs
|
// 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 SvelteSet(this.getParentsOfNode(node).map((n) => n.id));
|
const parents = new Set(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),
|
||||||
);
|
);
|
||||||
|
|
||||||
const edges = new SvelteMap<number, string[]>();
|
// get edges from this socket
|
||||||
this.getEdgesFromNode(node)
|
const edges = new Map(
|
||||||
.filter((e) => e[1] === index)
|
this.getEdgesFromNode(node)
|
||||||
.forEach((e) => {
|
.filter((e) => e[1] === index)
|
||||||
if (edges.has(e[2].id)) {
|
.map((e) => [e[2].id, e[3]]),
|
||||||
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];
|
||||||
|
|
||||||
@@ -779,8 +660,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
otherType.push(...(inputs[key].accepts || []));
|
otherType.push(...(inputs[key].accepts || []));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
areSocketsCompatible(ownType, otherType)
|
areSocketsCompatible(ownType, otherType) &&
|
||||||
&& !edges.get(node.id)?.includes(key)
|
edges.get(node.id) !== key
|
||||||
) {
|
) {
|
||||||
sockets.push([node, key]);
|
sockets.push([node, key]);
|
||||||
}
|
}
|
||||||
@@ -793,7 +674,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
removeEdge(
|
removeEdge(
|
||||||
edge: Edge,
|
edge: Edge,
|
||||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
|
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const id0 = edge[0].id;
|
const id0 = edge[0].id;
|
||||||
const sid0 = edge[1];
|
const sid0 = edge[1];
|
||||||
@@ -801,20 +682,21 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const sid2 = edge[3];
|
const sid2 = edge[3];
|
||||||
|
|
||||||
const _edge = this.edges.find(
|
const _edge = this.edges.find(
|
||||||
(e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
|
(e) =>
|
||||||
|
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!_edge) return;
|
if (!_edge) return;
|
||||||
|
|
||||||
if (edge[0].state.children) {
|
if (edge[0].state.children) {
|
||||||
edge[0].state.children = edge[0].state.children.filter(
|
edge[0].state.children = edge[0].state.children.filter(
|
||||||
(n: NodeInstance) => n.id !== id2
|
(n: NodeInstance) => n.id !== id2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (edge[2].state.parents) {
|
if (edge[2].state.parents) {
|
||||||
edge[2].state.parents = edge[2].state.parents.filter(
|
edge[2].state.parents = edge[2].state.parents.filter(
|
||||||
(n: NodeInstance) => n.id !== id0
|
(n: NodeInstance) => n.id !== id0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,6 +705,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.execute();
|
this.execute();
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getEdgesToNode(node: NodeInstance) {
|
getEdgesToNode(node: NodeInstance) {
|
||||||
|
|||||||
@@ -1,70 +1,36 @@
|
|||||||
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 { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
import type { OrthographicCamera, Vector3 } from 'three';
|
import type { GraphManager } from "./graph-manager.svelte";
|
||||||
import type { GraphManager } from './graph-manager.svelte';
|
import type { OrthographicCamera } from "three";
|
||||||
import { ColorGenerator } from './graph/colors';
|
|
||||||
import { getNodeHeight, getSocketPosition } from './helpers/nodeHelpers';
|
|
||||||
|
|
||||||
const graphStateKey = Symbol('graph-state');
|
|
||||||
|
const graphStateKey = Symbol("graph-state");
|
||||||
export function getGraphState() {
|
export function getGraphState() {
|
||||||
return getContext<GraphState>(graphStateKey);
|
return getContext<GraphState>(graphStateKey);
|
||||||
}
|
}
|
||||||
export function setGraphState(graphState: GraphState) {
|
export function setGraphState(graphState: GraphState) {
|
||||||
return setContext(graphStateKey, graphState);
|
return setContext(graphStateKey, graphState)
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphManagerKey = Symbol('graph-manager');
|
const graphManagerKey = Symbol("graph-manager");
|
||||||
export function getGraphManager() {
|
export function getGraphManager() {
|
||||||
return getContext<GraphManager>(graphManagerKey);
|
return getContext<GraphManager>(graphManagerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setGraphManager(manager: GraphManager) {
|
export function setGraphManager(manager: GraphManager) {
|
||||||
return setContext(graphManagerKey, manager);
|
return setContext(graphManagerKey, manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
type EdgeData = {
|
|
||||||
x1: number;
|
|
||||||
y1: number;
|
|
||||||
points: Vector3[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const predefinedColors = {
|
|
||||||
path: {
|
|
||||||
hue: 80,
|
|
||||||
lightness: 20,
|
|
||||||
saturation: 80
|
|
||||||
},
|
|
||||||
float: {
|
|
||||||
hue: 70,
|
|
||||||
lightness: 10,
|
|
||||||
saturation: 0
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
hue: 0,
|
|
||||||
lightness: 50,
|
|
||||||
saturation: 70
|
|
||||||
},
|
|
||||||
'*': {
|
|
||||||
hue: 200,
|
|
||||||
lightness: 20,
|
|
||||||
saturation: 100
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export class GraphState {
|
export class GraphState {
|
||||||
colors = new ColorGenerator(predefinedColors);
|
|
||||||
|
|
||||||
constructor(private graph: GraphManager) {
|
constructor(private graph: GraphManager) {
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem("cameraPosition", `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`)
|
||||||
'cameraPosition',
|
})
|
||||||
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
|
})
|
||||||
);
|
const storedPosition = localStorage.getItem("cameraPosition")
|
||||||
});
|
|
||||||
});
|
|
||||||
const storedPosition = localStorage.getItem('cameraPosition');
|
|
||||||
if (storedPosition) {
|
if (storedPosition) {
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(storedPosition);
|
const d = JSON.parse(storedPosition);
|
||||||
@@ -72,7 +38,7 @@ export class GraphState {
|
|||||||
this.cameraPosition[1] = d[1];
|
this.cameraPosition[1] = d[1];
|
||||||
this.cameraPosition[2] = d[2];
|
this.cameraPosition[2] = d[2];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed to parsed stored camera position', e);
|
console.log("Failed to parsed stored camera position", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,18 +46,13 @@ export class GraphState {
|
|||||||
width = $state(100);
|
width = $state(100);
|
||||||
height = $state(100);
|
height = $state(100);
|
||||||
|
|
||||||
hoveredEdgeId = $state<string | null>(null);
|
|
||||||
edges = new SvelteMap<string, EdgeData>();
|
|
||||||
|
|
||||||
wrapper = $state<HTMLDivElement>(null!);
|
wrapper = $state<HTMLDivElement>(null!);
|
||||||
rect: DOMRect = $derived(
|
rect: DOMRect = $derived(
|
||||||
(this.wrapper && this.width && this.height)
|
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0),
|
||||||
? this.wrapper.getBoundingClientRect()
|
|
||||||
: new DOMRect(0, 0, 0, 0)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
camera = $state<OrthographicCamera>(null!);
|
camera = $state<OrthographicCamera>(null!);
|
||||||
cameraPosition: [number, number, number] = $state([140, 100, 3.5]);
|
cameraPosition: [number, number, number] = $state([0, 0, 4]);
|
||||||
|
|
||||||
clipboard: null | {
|
clipboard: null | {
|
||||||
nodes: NodeInstance[];
|
nodes: NodeInstance[];
|
||||||
@@ -102,7 +63,7 @@ export class GraphState {
|
|||||||
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
|
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
|
||||||
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
|
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
|
||||||
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
|
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
|
||||||
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2
|
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
boxSelection = $state(false);
|
boxSelection = $state(false);
|
||||||
@@ -110,8 +71,8 @@ export class GraphState {
|
|||||||
addMenuPosition = $state<[number, number] | null>(null);
|
addMenuPosition = $state<[number, number] | null>(null);
|
||||||
|
|
||||||
snapToGrid = $state(false);
|
snapToGrid = $state(false);
|
||||||
backgroundType = $state<'grid' | 'dots' | 'none'>('grid');
|
showGrid = $state(true)
|
||||||
showHelp = $state(false);
|
showHelp = $state(false)
|
||||||
|
|
||||||
cameraDown = [0, 0];
|
cameraDown = [0, 0];
|
||||||
mouseDownNodeId = -1;
|
mouseDownNodeId = -1;
|
||||||
@@ -127,49 +88,33 @@ export class GraphState {
|
|||||||
hoveredSocket = $state<Socket | null>(null);
|
hoveredSocket = $state<Socket | null>(null);
|
||||||
possibleSockets = $state<Socket[]>([]);
|
possibleSockets = $state<Socket[]>([]);
|
||||||
possibleSocketIds = $derived(
|
possibleSocketIds = $derived(
|
||||||
new SvelteSet(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
|
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
getEdges() {
|
|
||||||
return $state.snapshot(this.edges);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
this.selectedNodes.clear();
|
this.selectedNodes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
|
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
|
||||||
|
|
||||||
setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
|
|
||||||
this.edges.set(edgeId, { x1, y1, points });
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEdgeGeometry(edgeId: string) {
|
|
||||||
this.edges.delete(edgeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdgeData() {
|
|
||||||
return this.edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNodePosition(node: NodeInstance) {
|
updateNodePosition(node: NodeInstance) {
|
||||||
if (
|
if (
|
||||||
node.state.x === node.position[0]
|
node.state.x === node.position[0] &&
|
||||||
&& node.state.y === node.position[1]
|
node.state.y === node.position[1]
|
||||||
) {
|
) {
|
||||||
delete node.state.x;
|
delete node.state.x;
|
||||||
delete node.state.y;
|
delete node.state.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
|
if (node.state["x"] !== undefined && node.state["y"] !== undefined) {
|
||||||
if (node.state.ref) {
|
if (node.state.ref) {
|
||||||
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
|
node.state.ref.style.setProperty("--nx", `${node.state.x * 10}px`);
|
||||||
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
|
node.state.ref.style.setProperty("--ny", `${node.state.y * 10}px`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (node.state.ref) {
|
if (node.state.ref) {
|
||||||
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
node.state.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
|
||||||
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
node.state.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,40 +127,58 @@ export class GraphState {
|
|||||||
return 4;
|
return 4;
|
||||||
} else if (z > 11) {
|
} else if (z > 11) {
|
||||||
return 2;
|
return 2;
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tryConnectToDebugNode(nodeId: number) {
|
getSocketPosition(
|
||||||
const node = this.graph.nodes.get(nodeId);
|
node: NodeInstance,
|
||||||
if (!node) return;
|
index: string | number,
|
||||||
if (node.type.endsWith('/debug')) return;
|
): [number, number] {
|
||||||
if (!node.state.type?.outputs?.length) return;
|
if (typeof index === "number") {
|
||||||
for (const _node of this.graph.nodes.values()) {
|
return [
|
||||||
if (_node.type.endsWith('/debug')) {
|
(node?.state?.x ?? node.position[0]) + 20,
|
||||||
this.graph.createEdge(node, 0, _node, 'input');
|
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index,
|
||||||
return;
|
];
|
||||||
}
|
} else {
|
||||||
}
|
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
||||||
|
return [
|
||||||
const debugNode = this.graph.createNode({
|
node?.state?.x ?? node.position[0],
|
||||||
type: 'max/plantarium/debug',
|
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index,
|
||||||
position: [node.position[0] + 30, node.position[1]],
|
];
|
||||||
props: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (debugNode) {
|
|
||||||
this.graph.createEdge(node, 0, debugNode, 'input');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyNodes() {
|
private nodeHeightCache: Record<string, number> = {};
|
||||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
|
getNodeHeight(nodeTypeId: string) {
|
||||||
return;
|
if (nodeTypeId in this.nodeHeightCache) {
|
||||||
|
return this.nodeHeightCache[nodeTypeId];
|
||||||
}
|
}
|
||||||
|
const node = this.graph.getNodeType(nodeTypeId);
|
||||||
|
if (!node?.inputs) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
const height =
|
||||||
|
5 +
|
||||||
|
10 *
|
||||||
|
Object.keys(node.inputs).filter(
|
||||||
|
(p) =>
|
||||||
|
p !== "seed" &&
|
||||||
|
node?.inputs &&
|
||||||
|
!("setting" in node?.inputs?.[p]) &&
|
||||||
|
node.inputs[p].hidden !== true,
|
||||||
|
).length;
|
||||||
|
this.nodeHeightCache[nodeTypeId] = height;
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyNodes() {
|
||||||
|
if (this.activeNodeId === -1 && !this.selectedNodes?.size)
|
||||||
|
return;
|
||||||
let nodes = [
|
let nodes = [
|
||||||
this.activeNodeId,
|
this.activeNodeId,
|
||||||
...(this.selectedNodes?.values() || [])
|
...(this.selectedNodes?.values() || []),
|
||||||
]
|
]
|
||||||
.map((id) => this.graph.getNode(id))
|
.map((id) => this.graph.getNode(id))
|
||||||
.filter(b => !!b);
|
.filter(b => !!b);
|
||||||
@@ -225,14 +188,14 @@ export class GraphState {
|
|||||||
...node,
|
...node,
|
||||||
position: [
|
position: [
|
||||||
this.mousePosition[0] - node.position[0],
|
this.mousePosition[0] - node.position[0],
|
||||||
this.mousePosition[1] - node.position[1]
|
this.mousePosition[1] - node.position[1],
|
||||||
],
|
],
|
||||||
tmp: undefined
|
tmp: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.clipboard = {
|
this.clipboard = {
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
edges: edges
|
edges: edges,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,19 +217,20 @@ export class GraphState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setDownSocket(socket: Socket) {
|
setDownSocket(socket: Socket) {
|
||||||
this.activeSocket = socket;
|
this.activeSocket = socket;
|
||||||
|
|
||||||
let { node, index, position } = socket;
|
let { node, index, position } = socket;
|
||||||
|
|
||||||
// remove existing edge
|
// 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) {
|
||||||
if (edge[3] === index) {
|
if (edge[3] === index) {
|
||||||
node = edge[0];
|
node = edge[0];
|
||||||
index = edge[1];
|
index = edge[1];
|
||||||
position = getSocketPosition(node, index);
|
position = this.getSocketPosition(node, index);
|
||||||
this.graph.removeEdge(edge);
|
this.graph.removeEdge(edge);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -277,7 +241,7 @@ export class GraphState {
|
|||||||
this.activeSocket = {
|
this.activeSocket = {
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
position
|
position,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.possibleSockets = this.graph
|
this.possibleSockets = this.graph
|
||||||
@@ -286,31 +250,32 @@ export class GraphState {
|
|||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
position: getSocketPosition(node, index)
|
position: this.getSocketPosition(node, index),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
projectScreenToWorld(x: number, y: number): [number, number] {
|
projectScreenToWorld(x: number, y: number): [number, number] {
|
||||||
return [
|
return [
|
||||||
this.cameraPosition[0]
|
this.cameraPosition[0] +
|
||||||
+ (x - this.width / 2) / this.cameraPosition[2],
|
(x - this.width / 2) / this.cameraPosition[2],
|
||||||
this.cameraPosition[1]
|
this.cameraPosition[1] +
|
||||||
+ (y - this.height / 2) / this.cameraPosition[2]
|
(y - this.height / 2) / this.cameraPosition[2],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeIdFromEvent(event: MouseEvent) {
|
getNodeIdFromEvent(event: MouseEvent) {
|
||||||
let clickedNodeId = -1;
|
let clickedNodeId = -1;
|
||||||
|
|
||||||
const mx = event.clientX - this.rect.x;
|
let mx = event.clientX - this.rect.x;
|
||||||
const my = event.clientY - this.rect.y;
|
let 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
|
||||||
if (event.target instanceof HTMLElement) {
|
if (event.target instanceof HTMLElement) {
|
||||||
const nodeElement = event.target.closest('.node');
|
const nodeElement = event.target.closest(".node");
|
||||||
const nodeId = nodeElement?.getAttribute?.('data-node-id');
|
const nodeId = nodeElement?.getAttribute?.("data-node-id");
|
||||||
if (nodeId) {
|
if (nodeId) {
|
||||||
clickedNodeId = parseInt(nodeId, 10);
|
clickedNodeId = parseInt(nodeId, 10);
|
||||||
}
|
}
|
||||||
@@ -323,7 +288,7 @@ export class GraphState {
|
|||||||
for (const node of this.graph.nodes.values()) {
|
for (const node of this.graph.nodes.values()) {
|
||||||
const x = node.position[0];
|
const x = node.position[0];
|
||||||
const y = node.position[1];
|
const y = node.position[1];
|
||||||
const height = getNodeHeight(node.state.type!);
|
const height = this.getNodeHeight(node.type);
|
||||||
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
|
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
|
||||||
clickedNodeId = node.id;
|
clickedNodeId = node.id;
|
||||||
break;
|
break;
|
||||||
@@ -335,15 +300,13 @@ export class GraphState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isNodeInView(node: NodeInstance) {
|
isNodeInView(node: NodeInstance) {
|
||||||
const height = getNodeHeight(node.state.type!);
|
const height = this.getNodeHeight(node.type);
|
||||||
const width = 20;
|
const width = 20;
|
||||||
return node.position[0] > this.cameraBounds[0] - width
|
return (
|
||||||
&& node.position[0] < this.cameraBounds[1]
|
node.position[0] > this.cameraBounds[0] - width &&
|
||||||
&& node.position[1] > this.cameraBounds[2] - height
|
node.position[0] < this.cameraBounds[1] &&
|
||||||
&& node.position[1] < this.cameraBounds[3];
|
node.position[1] > this.cameraBounds[2] - height &&
|
||||||
}
|
node.position[1] < this.cameraBounds[3]
|
||||||
|
);
|
||||||
openNodePalette() {
|
};
|
||||||
this.addMenuPosition = [this.mousePosition[0], this.mousePosition[1]];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +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 { createKeyMap } from "../../helpers/createKeyMap";
|
||||||
import { HTML } from '@threlte/extras';
|
import AddMenu from "../components/AddMenu.svelte";
|
||||||
import { createKeyMap } from '../../helpers/createKeyMap';
|
import Background from "../background/Background.svelte";
|
||||||
import Background from '../background/Background.svelte';
|
import BoxSelection from "../components/BoxSelection.svelte";
|
||||||
import AddMenu from '../components/AddMenu.svelte';
|
import EdgeEl from "../edges/Edge.svelte";
|
||||||
import BoxSelection from '../components/BoxSelection.svelte';
|
import NodeEl from "../node/Node.svelte";
|
||||||
import Camera from '../components/Camera.svelte';
|
import Camera from "../components/Camera.svelte";
|
||||||
import HelpView from '../components/HelpView.svelte';
|
import { Canvas } from "@threlte/core";
|
||||||
import Debug from '../debug/Debug.svelte';
|
import HelpView from "../components/HelpView.svelte";
|
||||||
import EdgeEl from '../edges/Edge.svelte';
|
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
||||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
import { HTML } from "@threlte/extras";
|
||||||
import { getSocketPosition } from '../helpers/nodeHelpers';
|
import { FileDropEventManager, MouseEventManager } from "./events";
|
||||||
import NodeEl from '../node/Node.svelte';
|
import { maxZoom, minZoom } from "./constants";
|
||||||
import { maxZoom, minZoom } from './constants';
|
|
||||||
import { FileDropEventManager } from './drop.events';
|
|
||||||
import { MouseEventManager } from './mouse.events';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
keymap,
|
keymap,
|
||||||
addMenuPadding
|
|
||||||
}: {
|
}: {
|
||||||
keymap: ReturnType<typeof createKeyMap>;
|
keymap: ReturnType<typeof createKeyMap>;
|
||||||
addMenuPadding?: { left?: number; right?: number; bottom?: number; top?: number };
|
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const graph = getGraphManager();
|
const graph = getGraphManager();
|
||||||
@@ -39,8 +34,8 @@
|
|||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pos1 = getSocketPosition(fromNode, edge[1]);
|
const pos1 = graphState.getSocketPosition(fromNode, edge[1]);
|
||||||
const pos2 = getSocketPosition(toNode, edge[3]);
|
const pos2 = graphState.getSocketPosition(toNode, edge[3]);
|
||||||
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,18 +43,19 @@
|
|||||||
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 = graphState.activeSocket.node.state?.type?.outputs?.[
|
const socketType =
|
||||||
graphState.activeSocket.index
|
graphState.activeSocket.node.state?.type?.outputs?.[
|
||||||
];
|
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) => inp[1].type === socketType,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
@@ -67,17 +63,18 @@
|
|||||||
graphState.activeSocket.node,
|
graphState.activeSocket.node,
|
||||||
graphState.activeSocket.index,
|
graphState.activeSocket.index,
|
||||||
newNode,
|
newNode,
|
||||||
input[0]
|
input[0],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const socketType = graphState.activeSocket.node.state?.type?.inputs?.[
|
const socketType =
|
||||||
graphState.activeSocket.index
|
graphState.activeSocket.node.state?.type?.inputs?.[
|
||||||
];
|
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 as string[])?.includes(out)) return true;
|
if (socketType?.accepts?.includes(out as any)) return true;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +83,7 @@
|
|||||||
newNode,
|
newNode,
|
||||||
output.indexOf(output),
|
output.indexOf(output),
|
||||||
graphState.activeSocket.node,
|
graphState.activeSocket.node,
|
||||||
graphState.activeSocket.index
|
graphState.activeSocket.index,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,25 +92,17 @@
|
|||||||
graphState.activeSocket = null;
|
graphState.activeSocket = null;
|
||||||
graphState.addMenuPosition = null;
|
graphState.addMenuPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSocketType(node: NodeInstance, index: number | string): string {
|
|
||||||
if (typeof index === 'string') {
|
|
||||||
return node.state.type?.inputs?.[index].type || 'unknown';
|
|
||||||
}
|
|
||||||
return node.state.type?.outputs?.[index] || 'unknown';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
onmousemove={(ev) => mouseEvents.handleWindowMouseMove(ev)}
|
onmousemove={(ev) => mouseEvents.handleMouseMove(ev)}
|
||||||
onmouseup={(ev) => mouseEvents.handleWindowMouseUp(ev)}
|
onmouseup={(ev) => mouseEvents.handleMouseUp(ev)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
|
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
|
||||||
bind:this={graphState.wrapper}
|
bind:this={graphState.wrapper}
|
||||||
class="graph-wrapper"
|
class="graph-wrapper"
|
||||||
style="height: 100%"
|
|
||||||
class:is-panning={graphState.isPanning}
|
class:is-panning={graphState.isPanning}
|
||||||
class:is-hovering={graphState.hoveredNodeId !== -1}
|
class:is-hovering={graphState.hoveredNodeId !== -1}
|
||||||
aria-label="Graph"
|
aria-label="Graph"
|
||||||
@@ -123,7 +112,6 @@
|
|||||||
bind:clientHeight={graphState.height}
|
bind:clientHeight={graphState.height}
|
||||||
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
||||||
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
||||||
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
|
|
||||||
{...fileDropEvents.getEventListenerProps()}
|
{...fileDropEvents.getEventListenerProps()}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -142,9 +130,8 @@
|
|||||||
position={graphState.cameraPosition}
|
position={graphState.cameraPosition}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if graphState.backgroundType !== 'none'}
|
{#if graphState.showGrid !== false}
|
||||||
<Background
|
<Background
|
||||||
type={graphState.backgroundType}
|
|
||||||
cameraPosition={graphState.cameraPosition}
|
cameraPosition={graphState.cameraPosition}
|
||||||
{maxZoom}
|
{maxZoom}
|
||||||
{minZoom}
|
{minZoom}
|
||||||
@@ -157,33 +144,27 @@
|
|||||||
<BoxSelection
|
<BoxSelection
|
||||||
cameraPosition={graphState.cameraPosition}
|
cameraPosition={graphState.cameraPosition}
|
||||||
p1={{
|
p1={{
|
||||||
x: graphState.cameraPosition[0]
|
x:
|
||||||
+ (graphState.mouseDown[0] - graphState.width / 2)
|
graphState.cameraPosition[0] +
|
||||||
/ graphState.cameraPosition[2],
|
(graphState.mouseDown[0] - graphState.width / 2) /
|
||||||
y: graphState.cameraPosition[1]
|
graphState.cameraPosition[2],
|
||||||
+ (graphState.mouseDown[1] - graphState.height / 2)
|
y:
|
||||||
/ graphState.cameraPosition[2]
|
graphState.cameraPosition[1] +
|
||||||
|
(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
|
<AddMenu onnode={handleNodeCreation} />
|
||||||
onnode={handleNodeCreation}
|
|
||||||
paddingTop={addMenuPadding?.top}
|
|
||||||
paddingRight={addMenuPadding?.right}
|
|
||||||
paddingBottom={addMenuPadding?.bottom}
|
|
||||||
paddingLeft={addMenuPadding?.left}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if graphState.activeSocket}
|
{#if graphState.activeSocket}
|
||||||
<EdgeEl
|
<EdgeEl
|
||||||
z={graphState.cameraPosition[2]}
|
z={graphState.cameraPosition[2]}
|
||||||
inputType={getSocketType(graphState.activeSocket.node, graphState.activeSocket.index)}
|
|
||||||
outputType={getSocketType(graphState.activeSocket.node, graphState.activeSocket.index)}
|
|
||||||
x1={graphState.activeSocket.position[0]}
|
x1={graphState.activeSocket.position[0]}
|
||||||
y1={graphState.activeSocket.position[1]}
|
y1={graphState.activeSocket.position[1]}
|
||||||
x2={graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0]}
|
x2={graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0]}
|
||||||
@@ -191,27 +172,14 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each graph.edges as edge (edge)}
|
{#each graph.edges as edge}
|
||||||
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
||||||
<EdgeEl
|
<EdgeEl z={graphState.cameraPosition[2]} {x1} {y1} {x2} {y2} />
|
||||||
id={graph.getEdgeId(edge)}
|
|
||||||
z={graphState.cameraPosition[2]}
|
|
||||||
inputType={getSocketType(edge[0], edge[1])}
|
|
||||||
outputType={getSocketType(edge[2], edge[3])}
|
|
||||||
{x1}
|
|
||||||
{y1}
|
|
||||||
{x2}
|
|
||||||
{y2}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<Debug />
|
<HTML>
|
||||||
|
|
||||||
<HTML transform={false}>
|
|
||||||
<div
|
<div
|
||||||
role="tree"
|
|
||||||
id="graph"
|
id="graph"
|
||||||
tabindex="0"
|
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
|
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
|
||||||
class:hovering-sockets={graphState.activeSocket}
|
class:hovering-sockets={graphState.activeSocket}
|
||||||
@@ -220,13 +188,14 @@
|
|||||||
<NodeEl
|
<NodeEl
|
||||||
{node}
|
{node}
|
||||||
inView={graphState.isNodeInView(node)}
|
inView={graphState.isNodeInView(node)}
|
||||||
|
z={graphState.cameraPosition[2]}
|
||||||
/>
|
/>
|
||||||
{/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>
|
||||||
@@ -264,7 +233,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--color-layer-2);
|
background: var(--layer-2);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
input:disabled {
|
input:disabled {
|
||||||
@@ -284,8 +253,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(--color-layer-2);
|
border: dashed 4px var(--layer-2);
|
||||||
background: var(--color-layer-1);
|
background: var(--layer-1);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createKeyMap } from '$lib/helpers/createKeyMap';
|
import type { Graph, NodeInstance, NodeRegistry } from "@nodarium/types";
|
||||||
import type { Graph, NodeInstance, NodeRegistry } from '@nodarium/types';
|
import GraphEl from "./Graph.svelte";
|
||||||
import { GraphManager } from '../graph-manager.svelte';
|
import { GraphManager } from "../graph-manager.svelte";
|
||||||
import { GraphState, setGraphManager, setGraphState } from '../graph-state.svelte';
|
import { createKeyMap } from "$lib/helpers/createKeyMap";
|
||||||
import { setupKeymaps } from '../keymaps';
|
import {
|
||||||
import GraphEl from './Graph.svelte';
|
GraphState,
|
||||||
|
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, unknown>;
|
settings?: Record<string, any>;
|
||||||
|
|
||||||
activeNode?: NodeInstance;
|
activeNode?: NodeInstance;
|
||||||
backgroundType?: 'grid' | 'dots' | 'none';
|
showGrid?: boolean;
|
||||||
snapToGrid?: boolean;
|
snapToGrid?: boolean;
|
||||||
showHelp?: boolean;
|
showHelp?: boolean;
|
||||||
settingTypes?: Record<string, unknown>;
|
settingTypes?: Record<string, any>;
|
||||||
|
|
||||||
addMenuPadding?: { left?: number; right?: number; bottom?: number; top?: number };
|
|
||||||
|
|
||||||
onsave?: (save: Graph) => void;
|
onsave?: (save: Graph) => void;
|
||||||
onresult?: (result: unknown) => void;
|
onresult?: (result: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
graph,
|
graph,
|
||||||
registry,
|
registry,
|
||||||
addMenuPadding,
|
|
||||||
settings = $bindable(),
|
settings = $bindable(),
|
||||||
activeNode = $bindable(),
|
activeNode = $bindable(),
|
||||||
backgroundType = $bindable('grid'),
|
showGrid = $bindable(true),
|
||||||
snapToGrid = $bindable(true),
|
snapToGrid = $bindable(true),
|
||||||
showHelp = $bindable(false),
|
showHelp = $bindable(false),
|
||||||
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);
|
||||||
|
|
||||||
const graphState = new GraphState(manager);
|
const graphState = new GraphState(manager);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
graphState.backgroundType = backgroundType;
|
graphState.showGrid = showGrid;
|
||||||
graphState.snapToGrid = snapToGrid;
|
graphState.snapToGrid = snapToGrid;
|
||||||
graphState.showHelp = showHelp;
|
graphState.showHelp = showHelp;
|
||||||
});
|
});
|
||||||
@@ -70,20 +70,22 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.on('settings', (_settings) => {
|
$effect(() => {
|
||||||
|
if (settingTypes && settings) {
|
||||||
|
manager.setSettings(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(() => {
|
manager.load(graph);
|
||||||
if (graph) {
|
|
||||||
manager.load(graph);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GraphEl {keymap} {addMenuPadding} />
|
<GraphEl {keymap} />
|
||||||
|
|||||||
@@ -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",
|
||||||
'connection'
|
"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!);
|
||||||
const color = style.getPropertyValue(`--color-${variable}`);
|
let color = style.getPropertyValue(`--${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(`--color-${v}`);
|
const hex = style.getPropertyValue(`--${v}`);
|
||||||
colors[v].setStyle(hex, LinearSRGBColorSpace);
|
colors[v].setStyle(hex, LinearSRGBColorSpace);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
type Color = { hue: number; saturation: number; lightness: number };
|
|
||||||
|
|
||||||
export class ColorGenerator {
|
|
||||||
private colors: Map<string, Color> = new Map();
|
|
||||||
private lightnessLevels = [10, 60];
|
|
||||||
|
|
||||||
constructor(predefined: Record<string, Color>) {
|
|
||||||
for (const [id, colorStr] of Object.entries(predefined)) {
|
|
||||||
this.colors.set(id, colorStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getColor(id: string): string {
|
|
||||||
if (this.colors.has(id)) {
|
|
||||||
return this.colorToHsl(this.colors.get(id)!);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newColor = this.generateNewColor();
|
|
||||||
this.colors.set(id, newColor);
|
|
||||||
return this.colorToHsl(newColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateNewColor(): Color {
|
|
||||||
const existingHues = Array.from(this.colors.values()).map(c => c.hue).sort();
|
|
||||||
let hue = existingHues[0];
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
while (
|
|
||||||
existingHues.some(h => Math.abs(h - hue) < 30 || Math.abs(h - hue) > 330)
|
|
||||||
&& attempts < 360
|
|
||||||
) {
|
|
||||||
hue = (hue + 30) % 360;
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lightness = 60;
|
|
||||||
|
|
||||||
return { hue, lightness, saturation: 100 };
|
|
||||||
}
|
|
||||||
|
|
||||||
private colorToHsl(c: Color): string {
|
|
||||||
return `hsl(${c.hue}, ${c.saturation}%, ${c.lightness}%)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { GraphSchema, type NodeId } from '@nodarium/types';
|
|
||||||
import type { GraphManager } from '../graph-manager.svelte';
|
|
||||||
import type { GraphState } from '../graph-state.svelte';
|
|
||||||
|
|
||||||
export class FileDropEventManager {
|
|
||||||
constructor(
|
|
||||||
private graph: GraphManager,
|
|
||||||
private state: GraphState
|
|
||||||
) {}
|
|
||||||
|
|
||||||
handleFileDrop(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.state.isDragging = false;
|
|
||||||
if (!event.dataTransfer) return;
|
|
||||||
const nodeId = event.dataTransfer.getData('data/node-id') as NodeId;
|
|
||||||
let mx = event.clientX - this.state.rect.x;
|
|
||||||
let my = event.clientY - this.state.rect.y;
|
|
||||||
|
|
||||||
if (nodeId) {
|
|
||||||
const nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
|
|
||||||
const nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
|
|
||||||
if (nodeOffsetX && nodeOffsetY) {
|
|
||||||
mx += parseInt(nodeOffsetX);
|
|
||||||
my += parseInt(nodeOffsetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
let props = {};
|
|
||||||
const rawNodeProps = event.dataTransfer.getData('data/node-props');
|
|
||||||
if (rawNodeProps) {
|
|
||||||
try {
|
|
||||||
props = JSON.parse(rawNodeProps);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse node dropped', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = this.state.projectScreenToWorld(mx, my);
|
|
||||||
this.graph.registry.load([nodeId]).then(() => {
|
|
||||||
this.graph.createNode({
|
|
||||||
type: nodeId,
|
|
||||||
props,
|
|
||||||
position: pos
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (event.dataTransfer.files.length) {
|
|
||||||
const file = event.dataTransfer.files[0];
|
|
||||||
|
|
||||||
if (file.type === 'application/wasm') {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
const buffer = e.target?.result;
|
|
||||||
if (buffer?.constructor === ArrayBuffer) {
|
|
||||||
const nodeType = await this.graph.registry.register(nodeId, buffer);
|
|
||||||
|
|
||||||
this.graph.createNode({
|
|
||||||
type: nodeType.id,
|
|
||||||
props: {},
|
|
||||||
position: this.state.projectScreenToWorld(mx, my)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
} else if (file.type === 'application/json') {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const buffer = e.target?.result as ArrayBuffer;
|
|
||||||
if (buffer) {
|
|
||||||
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
|
||||||
this.graph.load(state);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseLeave() {
|
|
||||||
this.state.isDragging = false;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragEnter(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.state.isDragging = true;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragOver(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.state.isDragging = true;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragEnd(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.state.isDragging = true;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEventListenerProps() {
|
|
||||||
return {
|
|
||||||
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
|
||||||
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
|
||||||
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
|
||||||
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
|
||||||
onmouseleave: () => this.handleMouseLeave()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import type { Box } from '@nodarium/types';
|
|
||||||
import type { GraphManager } from '../graph-manager.svelte';
|
|
||||||
import type { GraphState } from '../graph-state.svelte';
|
|
||||||
import { distanceFromPointToSegment } from '../helpers';
|
|
||||||
|
|
||||||
export class EdgeInteractionManager {
|
|
||||||
constructor(
|
|
||||||
private graph: GraphManager,
|
|
||||||
private state: GraphState
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private MIN_DISTANCE = 3;
|
|
||||||
|
|
||||||
private _boundingBoxes = new Map<string, Box>();
|
|
||||||
|
|
||||||
handleMouseDown() {
|
|
||||||
this._boundingBoxes.clear();
|
|
||||||
|
|
||||||
const possibleEdges = this.graph
|
|
||||||
.getPossibleDropOnEdges(this.state.activeNodeId)
|
|
||||||
.map(e => this.graph.getEdgeId(e));
|
|
||||||
|
|
||||||
const edges = this.state.getEdges();
|
|
||||||
for (const edge of edges) {
|
|
||||||
const edgeId = edge[0];
|
|
||||||
if (!possibleEdges.includes(edgeId)) {
|
|
||||||
edges.delete(edgeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [edgeId, data] of edges) {
|
|
||||||
const points = data.points;
|
|
||||||
let minX = points[0].x + data.x1;
|
|
||||||
let maxX = points[0].x + data.x1;
|
|
||||||
let minY = points[0].z + data.y1;
|
|
||||||
let maxY = points[0].z + data.y1;
|
|
||||||
|
|
||||||
// Iterate through all points to find the true bounds
|
|
||||||
for (let i = 0; i < points.length; i++) {
|
|
||||||
const x = data.x1 + points[i].x;
|
|
||||||
const y = data.y1 + points[i].z;
|
|
||||||
if (x < minX) minX = x;
|
|
||||||
if (x > maxX) maxX = x;
|
|
||||||
if (y < minY) minY = y;
|
|
||||||
if (y > maxY) maxY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundingBox = {
|
|
||||||
minX: minX - this.MIN_DISTANCE,
|
|
||||||
maxX: maxX + this.MIN_DISTANCE,
|
|
||||||
minY: minY - this.MIN_DISTANCE,
|
|
||||||
maxY: maxY + this.MIN_DISTANCE
|
|
||||||
};
|
|
||||||
|
|
||||||
this._boundingBoxes.set(edgeId, boundingBox);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseMove() {
|
|
||||||
const [mouseX, mouseY] = this.state.mousePosition;
|
|
||||||
const hoveredEdgeIds: string[] = [];
|
|
||||||
|
|
||||||
const edges = this.state.getEdges();
|
|
||||||
|
|
||||||
// Check if mouse is inside any bounding box
|
|
||||||
for (const [edgeId, box] of this._boundingBoxes) {
|
|
||||||
const isInside = mouseX >= box.minX
|
|
||||||
&& mouseX <= box.maxX
|
|
||||||
&& mouseY >= box.minY
|
|
||||||
&& mouseY <= box.maxY;
|
|
||||||
|
|
||||||
if (isInside) {
|
|
||||||
hoveredEdgeIds.push(edgeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let hoveredEdgeId: string | null = null;
|
|
||||||
let hoveredEdgeDistance = Infinity;
|
|
||||||
|
|
||||||
const DENSITY = 10; // higher DENSITY = less points checked (yes density might not be the best name :-)
|
|
||||||
for (const edgeId of hoveredEdgeIds) {
|
|
||||||
const edge = edges.get(edgeId)!;
|
|
||||||
for (let i = 0; i < edge.points.length - DENSITY; i += DENSITY) {
|
|
||||||
const pointAx = edge.points[i].x + edge.x1;
|
|
||||||
const pointAy = edge.points[i].z + edge.y1;
|
|
||||||
const pointBx = edge.points[i + DENSITY].x + edge.x1;
|
|
||||||
const pointBy = edge.points[i + DENSITY].z + edge.y1;
|
|
||||||
const distance = distanceFromPointToSegment(
|
|
||||||
pointAx,
|
|
||||||
pointAy,
|
|
||||||
pointBx,
|
|
||||||
pointBy,
|
|
||||||
mouseX,
|
|
||||||
mouseY
|
|
||||||
);
|
|
||||||
if (distance < this.MIN_DISTANCE) {
|
|
||||||
if (distance < hoveredEdgeDistance) {
|
|
||||||
hoveredEdgeDistance = distance;
|
|
||||||
hoveredEdgeId = edgeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.hoveredEdgeId = hoveredEdgeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseUp() {
|
|
||||||
if (this.state.hoveredEdgeId) {
|
|
||||||
const edge = this.graph.getEdgeById(this.state.hoveredEdgeId);
|
|
||||||
if (edge) {
|
|
||||||
this.graph.dropNodeOnEdge(this.state.activeNodeId, edge);
|
|
||||||
}
|
|
||||||
this.state.hoveredEdgeId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,127 @@
|
|||||||
import { animate, lerp } from '$lib/helpers';
|
import { GraphSchema, type NodeId, type NodeInstance } from "@nodarium/types";
|
||||||
import { type NodeInstance } from '@nodarium/types';
|
import type { GraphManager } from "../graph-manager.svelte";
|
||||||
import type { GraphManager } from '../graph-manager.svelte';
|
import type { GraphState } from "../graph-state.svelte";
|
||||||
import { type GraphState } from '../graph-state.svelte';
|
import { animate, lerp } from "$lib/helpers";
|
||||||
import { snapToGrid as snapPointToGrid } from '../helpers';
|
import { snapToGrid as snapPointToGrid } from "../helpers";
|
||||||
import { getNodeHeight } from '../helpers/nodeHelpers';
|
import { maxZoom, minZoom, zoomSpeed } from "./constants";
|
||||||
import { maxZoom, minZoom, zoomSpeed } from './constants';
|
|
||||||
import { EdgeInteractionManager } from './edge.events';
|
|
||||||
|
|
||||||
export class MouseEventManager {
|
|
||||||
edgeInteractionManager: EdgeInteractionManager;
|
export class FileDropEventManager {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private graph: GraphManager,
|
private graph: GraphManager,
|
||||||
private state: GraphState
|
private state: GraphState
|
||||||
) {
|
) { }
|
||||||
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
|
|
||||||
|
handleFileDrop(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.state.isDragging = false;
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
const nodeId = event.dataTransfer.getData("data/node-id") as NodeId;
|
||||||
|
let mx = event.clientX - this.state.rect.x;
|
||||||
|
let my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
|
if (nodeId) {
|
||||||
|
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
|
||||||
|
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
|
||||||
|
if (nodeOffsetX && nodeOffsetY) {
|
||||||
|
mx += parseInt(nodeOffsetX);
|
||||||
|
my += parseInt(nodeOffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
let rawNodeProps = event.dataTransfer.getData("data/node-props");
|
||||||
|
if (rawNodeProps) {
|
||||||
|
try {
|
||||||
|
props = JSON.parse(rawNodeProps);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.state.projectScreenToWorld(mx, my);
|
||||||
|
this.graph.registry.load([nodeId]).then(() => {
|
||||||
|
this.graph.createNode({
|
||||||
|
type: nodeId,
|
||||||
|
props,
|
||||||
|
position: pos,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (event.dataTransfer.files.length) {
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
|
||||||
|
if (file.type === "application/wasm") {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const buffer = e.target?.result;
|
||||||
|
if (buffer?.constructor === ArrayBuffer) {
|
||||||
|
const nodeType = await this.graph.registry.register(buffer);
|
||||||
|
|
||||||
|
this.graph.createNode({
|
||||||
|
type: nodeType.id,
|
||||||
|
props: {},
|
||||||
|
position: this.state.projectScreenToWorld(mx, my),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
} else if (file.type === "application/json") {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const buffer = e.target?.result as ArrayBuffer;
|
||||||
|
if (buffer) {
|
||||||
|
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
||||||
|
this.graph.load(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowMouseUp(event: MouseEvent) {
|
handleMouseLeave() {
|
||||||
this.edgeInteractionManager.handleMouseUp();
|
this.state.isDragging = false;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnter(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventListenerProps() {
|
||||||
|
return {
|
||||||
|
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
||||||
|
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
||||||
|
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
||||||
|
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
||||||
|
onmouseleave: () => this.handleMouseLeave(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class MouseEventManager {
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private graph: GraphManager,
|
||||||
|
private state: GraphState
|
||||||
|
) { }
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseUp(event: MouseEvent) {
|
||||||
this.state.isPanning = false;
|
this.state.isPanning = false;
|
||||||
if (!this.state.mouseDown) return;
|
if (!this.state.mouseDown) return;
|
||||||
|
|
||||||
@@ -42,23 +145,25 @@ export class MouseEventManager {
|
|||||||
const snapLevel = this.state.getSnapLevel();
|
const snapLevel = this.state.getSnapLevel();
|
||||||
activeNode.position[0] = snapPointToGrid(
|
activeNode.position[0] = snapPointToGrid(
|
||||||
activeNode?.state?.x ?? activeNode.position[0],
|
activeNode?.state?.x ?? activeNode.position[0],
|
||||||
5 / snapLevel
|
5 / snapLevel,
|
||||||
);
|
);
|
||||||
activeNode.position[1] = snapPointToGrid(
|
activeNode.position[1] = snapPointToGrid(
|
||||||
activeNode?.state?.y ?? activeNode.position[1],
|
activeNode?.state?.y ?? activeNode.position[1],
|
||||||
5 / snapLevel
|
5 / snapLevel,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
||||||
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
||||||
}
|
}
|
||||||
const nodes = [
|
const nodes = [
|
||||||
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
|
...[...(this.state.selectedNodes?.values() || [])].map((id) =>
|
||||||
|
this.graph.getNode(id),
|
||||||
|
),
|
||||||
] as NodeInstance[];
|
] as NodeInstance[];
|
||||||
|
|
||||||
const vec = [
|
const vec = [
|
||||||
activeNode.position[0] - (activeNode?.state.x || 0),
|
activeNode.position[0] - (activeNode?.state.x || 0),
|
||||||
activeNode.position[1] - (activeNode?.state.y || 0)
|
activeNode.position[1] - (activeNode?.state.y || 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -74,9 +179,9 @@ export class MouseEventManager {
|
|||||||
animate(500, (a: number) => {
|
animate(500, (a: number) => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (
|
if (
|
||||||
node?.state
|
node?.state &&
|
||||||
&& node.state['x'] !== undefined
|
node.state["x"] !== undefined &&
|
||||||
&& node.state['y'] !== undefined
|
node.state["y"] !== undefined
|
||||||
) {
|
) {
|
||||||
node.state.x = lerp(node.state.x, node.position[0], a);
|
node.state.x = lerp(node.state.x, node.position[0], a);
|
||||||
node.state.y = lerp(node.state.y, node.position[1], a);
|
node.state.y = lerp(node.state.y, node.position[1], a);
|
||||||
@@ -90,24 +195,24 @@ export class MouseEventManager {
|
|||||||
this.graph.save();
|
this.graph.save();
|
||||||
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
||||||
if (
|
if (
|
||||||
typeof this.state.hoveredSocket.index === 'number'
|
typeof this.state.hoveredSocket.index === "number" &&
|
||||||
&& typeof this.state.activeSocket.index === 'string'
|
typeof this.state.activeSocket.index === "string"
|
||||||
) {
|
) {
|
||||||
this.graph.createEdge(
|
this.graph.createEdge(
|
||||||
this.state.hoveredSocket.node,
|
this.state.hoveredSocket.node,
|
||||||
this.state.hoveredSocket.index || 0,
|
this.state.hoveredSocket.index || 0,
|
||||||
this.state.activeSocket.node,
|
this.state.activeSocket.node,
|
||||||
this.state.activeSocket.index
|
this.state.activeSocket.index,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
typeof this.state.activeSocket.index == 'number'
|
typeof this.state.activeSocket.index == "number" &&
|
||||||
&& typeof this.state.hoveredSocket.index === 'string'
|
typeof this.state.hoveredSocket.index === "string"
|
||||||
) {
|
) {
|
||||||
this.graph.createEdge(
|
this.graph.createEdge(
|
||||||
this.state.activeSocket.node,
|
this.state.activeSocket.node,
|
||||||
this.state.activeSocket.index || 0,
|
this.state.activeSocket.index || 0,
|
||||||
this.state.hoveredSocket.node,
|
this.state.hoveredSocket.node,
|
||||||
this.state.hoveredSocket.index
|
this.state.hoveredSocket.index,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.graph.save();
|
this.graph.save();
|
||||||
@@ -115,18 +220,18 @@ export class MouseEventManager {
|
|||||||
// Handle automatic adding of nodes on ctrl+mouseUp
|
// Handle automatic adding of nodes on ctrl+mouseUp
|
||||||
this.state.edgeEndPosition = [
|
this.state.edgeEndPosition = [
|
||||||
this.state.mousePosition[0],
|
this.state.mousePosition[0],
|
||||||
this.state.mousePosition[1]
|
this.state.mousePosition[1],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (typeof this.state.activeSocket.index === 'number') {
|
if (typeof this.state.activeSocket.index === "number") {
|
||||||
this.state.addMenuPosition = [
|
this.state.addMenuPosition = [
|
||||||
this.state.mousePosition[0],
|
this.state.mousePosition[0],
|
||||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
this.state.addMenuPosition = [
|
this.state.addMenuPosition = [
|
||||||
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
|
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
|
||||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -134,11 +239,11 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// check if camera moved
|
// check if camera moved
|
||||||
if (
|
if (
|
||||||
clickedNodeId === -1
|
clickedNodeId === -1 &&
|
||||||
&& !this.state.boxSelection
|
!this.state.boxSelection &&
|
||||||
&& this.state.cameraDown[0] === this.state.cameraPosition[0]
|
this.state.cameraDown[0] === this.state.cameraPosition[0] &&
|
||||||
&& this.state.cameraDown[1] === this.state.cameraPosition[1]
|
this.state.cameraDown[1] === this.state.cameraPosition[1] &&
|
||||||
&& this.state.isBodyFocused()
|
this.state.isBodyFocused()
|
||||||
) {
|
) {
|
||||||
this.state.activeNodeId = -1;
|
this.state.activeNodeId = -1;
|
||||||
this.state.clearSelection();
|
this.state.clearSelection();
|
||||||
@@ -152,33 +257,23 @@ export class MouseEventManager {
|
|||||||
this.state.addMenuPosition = null;
|
this.state.addMenuPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu(event: MouseEvent) {
|
|
||||||
if (!this.state.addMenuPosition) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.state.openNodePalette();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown(event: MouseEvent) {
|
handleMouseDown(event: MouseEvent) {
|
||||||
// Right click
|
|
||||||
if (event.button === 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.mouseDown) return;
|
if (this.state.mouseDown) return;
|
||||||
this.state.edgeEndPosition = null;
|
this.state.edgeEndPosition = null;
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
|
|
||||||
if (
|
if (event.target instanceof HTMLElement) {
|
||||||
target.nodeName !== 'CANVAS'
|
if (
|
||||||
&& !target.classList.contains('node')
|
event.target.nodeName !== "CANVAS" &&
|
||||||
&& !target.classList.contains('content')
|
!event.target.classList.contains("node") &&
|
||||||
) {
|
!event.target.classList.contains("content")
|
||||||
return;
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mx = event.clientX - this.state.rect.x;
|
let mx = event.clientX - this.state.rect.x;
|
||||||
const my = event.clientY - this.state.rect.y;
|
let 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];
|
||||||
@@ -189,15 +284,11 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// if we clicked on a node
|
// if we clicked on a node
|
||||||
if (clickedNodeId !== -1) {
|
if (clickedNodeId !== -1) {
|
||||||
if (event.ctrlKey && event.shiftKey) {
|
|
||||||
this.state.tryConnectToDebugNode(clickedNodeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.state.activeNodeId === -1) {
|
if (this.state.activeNodeId === -1) {
|
||||||
this.state.activeNodeId = clickedNodeId;
|
this.state.activeNodeId = clickedNodeId;
|
||||||
// if the selected node is the same as the clicked node
|
// if the selected node is the same as the clicked node
|
||||||
} else if (this.state.activeNodeId === clickedNodeId) {
|
} else if (this.state.activeNodeId === clickedNodeId) {
|
||||||
// $activeNodeId = -1;
|
//$activeNodeId = -1;
|
||||||
// if the clicked node is different from the selected node and secondary
|
// if the clicked node is different from the selected node and secondary
|
||||||
} else if (event.ctrlKey) {
|
} else if (event.ctrlKey) {
|
||||||
this.state.selectedNodes.add(this.state.activeNodeId);
|
this.state.selectedNodes.add(this.state.activeNodeId);
|
||||||
@@ -221,7 +312,6 @@ export class MouseEventManager {
|
|||||||
this.state.activeNodeId = clickedNodeId;
|
this.state.activeNodeId = clickedNodeId;
|
||||||
this.state.clearSelection();
|
this.state.clearSelection();
|
||||||
}
|
}
|
||||||
this.edgeInteractionManager.handleMouseDown();
|
|
||||||
} else if (event.ctrlKey) {
|
} else if (event.ctrlKey) {
|
||||||
this.state.boxSelection = true;
|
this.state.boxSelection = true;
|
||||||
}
|
}
|
||||||
@@ -245,9 +335,10 @@ export class MouseEventManager {
|
|||||||
this.state.edgeEndPosition = null;
|
this.state.edgeEndPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowMouseMove(event: MouseEvent) {
|
|
||||||
const mx = event.clientX - this.state.rect.x;
|
handleMouseMove(event: MouseEvent) {
|
||||||
const my = event.clientY - this.state.rect.y;
|
let mx = event.clientX - this.state.rect.x;
|
||||||
|
let 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);
|
||||||
@@ -260,8 +351,8 @@ export class MouseEventManager {
|
|||||||
let _socket;
|
let _socket;
|
||||||
for (const socket of this.state.possibleSockets) {
|
for (const socket of this.state.possibleSockets) {
|
||||||
const dist = Math.sqrt(
|
const dist = Math.sqrt(
|
||||||
(socket.position[0] - this.state.mousePosition[0]) ** 2
|
(socket.position[0] - this.state.mousePosition[0]) ** 2 +
|
||||||
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
|
(socket.position[1] - this.state.mousePosition[1]) ** 2,
|
||||||
);
|
);
|
||||||
if (dist < smallestDist) {
|
if (dist < smallestDist) {
|
||||||
smallestDist = dist;
|
smallestDist = dist;
|
||||||
@@ -269,7 +360,7 @@ export class MouseEventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_socket && smallestDist < 1.5) {
|
if (_socket && smallestDist < 0.9) {
|
||||||
this.state.mousePosition = _socket.position;
|
this.state.mousePosition = _socket.position;
|
||||||
this.state.hoveredSocket = _socket;
|
this.state.hoveredSocket = _socket;
|
||||||
} else {
|
} else {
|
||||||
@@ -284,7 +375,7 @@ export class MouseEventManager {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const mouseD = this.state.projectScreenToWorld(
|
const mouseD = this.state.projectScreenToWorld(
|
||||||
this.state.mouseDown[0],
|
this.state.mouseDown[0],
|
||||||
this.state.mouseDown[1]
|
this.state.mouseDown[1],
|
||||||
);
|
);
|
||||||
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
||||||
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
||||||
@@ -294,7 +385,7 @@ export class MouseEventManager {
|
|||||||
if (!node?.state) continue;
|
if (!node?.state) continue;
|
||||||
const x = node.position[0];
|
const x = node.position[0];
|
||||||
const y = node.position[1];
|
const y = node.position[1];
|
||||||
const height = getNodeHeight(node.state.type!);
|
const height = this.state.getNodeHeight(node.type);
|
||||||
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
||||||
this.state.selectedNodes?.add(node.id);
|
this.state.selectedNodes?.add(node.id);
|
||||||
} else {
|
} else {
|
||||||
@@ -306,7 +397,6 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// here we are handling dragging of nodes
|
// here we are handling dragging of nodes
|
||||||
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
|
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
|
||||||
this.edgeInteractionManager.handleMouseMove();
|
|
||||||
const node = this.graph.getNode(this.state.activeNodeId);
|
const node = this.graph.getNode(this.state.activeNodeId);
|
||||||
if (!node || event.buttons !== 1) return;
|
if (!node || event.buttons !== 1) return;
|
||||||
|
|
||||||
@@ -315,8 +405,10 @@ export class MouseEventManager {
|
|||||||
const oldX = node.state.downX || 0;
|
const oldX = node.state.downX || 0;
|
||||||
const oldY = node.state.downY || 0;
|
const oldY = node.state.downY || 0;
|
||||||
|
|
||||||
let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
let newX =
|
||||||
let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
|
let newY =
|
||||||
|
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
const snapLevel = this.state.getSnapLevel();
|
const snapLevel = this.state.getSnapLevel();
|
||||||
@@ -356,19 +448,23 @@ 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;
|
||||||
const newX = this.state.cameraDown[0]
|
let newX =
|
||||||
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
this.state.cameraDown[0] -
|
||||||
const newY = this.state.cameraDown[1]
|
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
let newY =
|
||||||
|
this.state.cameraDown[1] -
|
||||||
|
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
|
|
||||||
this.state.cameraPosition[0] = newX;
|
this.state.cameraPosition[0] = newX;
|
||||||
this.state.cameraPosition[1] = newY;
|
this.state.cameraPosition[1] = newY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleMouseScroll(event: WheelEvent) {
|
handleMouseScroll(event: WheelEvent) {
|
||||||
const bodyIsFocused = document.activeElement === document.body
|
const bodyIsFocused =
|
||||||
|| document.activeElement === this.state.wrapper
|
document.activeElement === document.body ||
|
||||||
|| document?.activeElement?.id === 'graph';
|
document.activeElement === this.state.wrapper ||
|
||||||
|
document?.activeElement?.id === "graph";
|
||||||
if (!bodyIsFocused) return;
|
if (!bodyIsFocused) return;
|
||||||
|
|
||||||
// Define zoom speed and clamp it between -1 and 1
|
// Define zoom speed and clamp it between -1 and 1
|
||||||
@@ -383,20 +479,21 @@ export class MouseEventManager {
|
|||||||
maxZoom,
|
maxZoom,
|
||||||
isNegative
|
isNegative
|
||||||
? this.state.cameraPosition[2] / delta
|
? this.state.cameraPosition[2] / delta
|
||||||
: this.state.cameraPosition[2] * delta
|
: this.state.cameraPosition[2] * delta,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate the ratio of the new zoom to the original zoom
|
// Calculate the ratio of the new zoom to the original zoom
|
||||||
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
||||||
|
|
||||||
// Update camera position and zoom level
|
// Update camera position and zoom level
|
||||||
this.state.cameraPosition[0] = this.state.mousePosition[0]
|
this.state.cameraPosition[0] = this.state.mousePosition[0] -
|
||||||
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
|
(this.state.mousePosition[0] - this.state.cameraPosition[0]) /
|
||||||
/ 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;
|
zoomRatio,
|
||||||
this.state.cameraPosition[2] = newZoom;
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
|
|||||||
|
|
||||||
export function animate(
|
export function animate(
|
||||||
duration: number,
|
duration: number,
|
||||||
callback: (progress: number) => void | false
|
callback: (progress: number) => void | false,
|
||||||
) {
|
) {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const loop = (time: number) => {
|
const loop = (time: number) => {
|
||||||
@@ -33,53 +33,59 @@ export function createNodePath({
|
|||||||
cornerBottom = 0,
|
cornerBottom = 0,
|
||||||
leftBump = false,
|
leftBump = false,
|
||||||
rightBump = false,
|
rightBump = false,
|
||||||
aspectRatio = 1
|
aspectRatio = 1,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const leftBumpTopY = y + height / 2;
|
|
||||||
const leftBumpBottomY = y - height / 2;
|
|
||||||
|
|
||||||
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}
|
||||||
Q100,0 100,${cornerTop}
|
Q100,0 100,${cornerTop}
|
||||||
`
|
`
|
||||||
: ` 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
|
leftBump
|
||||||
? ` V${leftBumpTopY} C${depth},${leftBumpTopY} ${depth},${leftBumpBottomY} 0,${leftBumpBottomY}`
|
? ` 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 clone: <T>(v: T) => T = 'structedClone' in globalThis
|
export const debounce = (fn: Function, ms = 300) => {
|
||||||
? globalThis.structuredClone
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
return function (this: any, ...args: any[]) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function withSubComponents<A, B extends Record<string, unknown>>(
|
export const clone: <T>(v: T) => T =
|
||||||
|
"structedClone" in globalThis
|
||||||
|
? globalThis.structuredClone
|
||||||
|
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
|
export function withSubComponents<A, B extends Record<string, any>>(
|
||||||
component: A,
|
component: A,
|
||||||
subcomponents: B
|
subcomponents: B,
|
||||||
): A & B {
|
): A & B {
|
||||||
Object.keys(subcomponents).forEach((key) => {
|
Object.keys(subcomponents).forEach((key) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -87,27 +93,3 @@ export function withSubComponents<A, B extends Record<string, unknown>>(
|
|||||||
});
|
});
|
||||||
return component as A & B;
|
return component as A & B;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function distanceFromPointToSegment(
|
|
||||||
x1: number,
|
|
||||||
y1: number,
|
|
||||||
x2: number,
|
|
||||||
y2: number,
|
|
||||||
x0: number,
|
|
||||||
y0: number
|
|
||||||
): number {
|
|
||||||
const dx = x2 - x1;
|
|
||||||
const dy = y2 - y1;
|
|
||||||
|
|
||||||
if (dx === 0 && dy === 0) {
|
|
||||||
return Math.hypot(x0 - x1, y0 - y1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = ((x0 - x1) * dx + (y0 - y1) * dy) / (dx * dx + dy * dy);
|
|
||||||
const clampedT = Math.max(0, Math.min(1, t));
|
|
||||||
|
|
||||||
const px = x1 + clampedT * dx;
|
|
||||||
const py = y1 + clampedT * dy;
|
|
||||||
|
|
||||||
return Math.hypot(x0 - px, y0 - py);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { type Writable, writable } from 'svelte/store';
|
import { writable, type 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) {
|
||||||
@@ -35,15 +36,18 @@ 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,71 +0,0 @@
|
|||||||
import type { NodeDefinition, NodeInstance } from '@nodarium/types';
|
|
||||||
|
|
||||||
export function getParameterHeight(node: NodeDefinition, inputKey: string) {
|
|
||||||
const input = node.inputs?.[inputKey];
|
|
||||||
if (!input) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputKey === 'seed') return 0;
|
|
||||||
if (!node.inputs) return 0;
|
|
||||||
if ('setting' in input) return 0;
|
|
||||||
if (input.hidden) return 0;
|
|
||||||
|
|
||||||
if (input.type === 'shape' && input.external !== true) {
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
input?.label !== '' && !input.external && input.type !== 'path'
|
|
||||||
&& input.type !== 'geometry'
|
|
||||||
) {
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSocketPosition(
|
|
||||||
node: NodeInstance,
|
|
||||||
index: string | number
|
|
||||||
): [number, number] {
|
|
||||||
if (typeof index === 'number') {
|
|
||||||
return [
|
|
||||||
(node?.state?.x ?? node.position[0]) + 20,
|
|
||||||
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
let height = 5;
|
|
||||||
const nodeType = node.state.type!;
|
|
||||||
const inputs = nodeType.inputs || {};
|
|
||||||
for (const inputKey in inputs) {
|
|
||||||
const h = getParameterHeight(nodeType, inputKey) / 10;
|
|
||||||
if (inputKey === index) {
|
|
||||||
height += h / 2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
height += h;
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
node?.state?.x ?? node.position[0],
|
|
||||||
(node?.state?.y ?? node.position[1]) + height
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeHeightCache: Record<string, number> = {};
|
|
||||||
export function getNodeHeight(node: NodeDefinition) {
|
|
||||||
if (node.id in nodeHeightCache) {
|
|
||||||
return nodeHeightCache[node.id];
|
|
||||||
}
|
|
||||||
if (!node?.inputs) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
let height = 5;
|
|
||||||
|
|
||||||
for (const key in node.inputs) {
|
|
||||||
const h = getParameterHeight(node, key) / 10;
|
|
||||||
height += h;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeHeightCache[node.id] = height;
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import type { Graph } from '@nodarium/types';
|
import { create, type Delta } from "jsondiffpatch";
|
||||||
import { createLogger } from '@nodarium/utils';
|
import type { Graph } from "@nodarium/types";
|
||||||
import { create, type Delta } from 'jsondiffpatch';
|
import { clone } from "./helpers/index.js";
|
||||||
import { clone } from './helpers/index.js';
|
import { createLogger } from "@nodarium/utils";
|
||||||
|
|
||||||
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;
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
return obj.join('-');
|
return obj.join("-");
|
||||||
}
|
}
|
||||||
return '$$index:' + index;
|
return "$$index:" + index;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const log = createLogger('history');
|
const log = createLogger("history");
|
||||||
log.mute();
|
log.mute();
|
||||||
|
|
||||||
export class HistoryManager {
|
export class HistoryManager {
|
||||||
@@ -26,7 +26,7 @@ export class HistoryManager {
|
|||||||
|
|
||||||
private opts = {
|
private opts = {
|
||||||
debounce: 400,
|
debounce: 400,
|
||||||
maxHistory: 100
|
maxHistory: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor({ maxHistory = 100, debounce = 100 } = {}) {
|
constructor({ maxHistory = 100, debounce = 100 } = {}) {
|
||||||
@@ -40,12 +40,12 @@ export class HistoryManager {
|
|||||||
if (!this.state) {
|
if (!this.state) {
|
||||||
this.state = clone(state);
|
this.state = clone(state);
|
||||||
this.initialState = this.state;
|
this.initialState = this.state;
|
||||||
log.log('initial state saved');
|
log.log("initial state saved");
|
||||||
} else {
|
} else {
|
||||||
const newState = state;
|
const newState = state;
|
||||||
const delta = diff.diff(this.state, newState);
|
const delta = diff.diff(this.state, newState);
|
||||||
if (delta) {
|
if (delta) {
|
||||||
log.log('saving state');
|
log.log("saving state");
|
||||||
// Add the delta to history
|
// Add the delta to history
|
||||||
if (this.index < this.history.length - 1) {
|
if (this.index < this.history.length - 1) {
|
||||||
// Clear the history after the current index if new changes are made
|
// Clear the history after the current index if new changes are made
|
||||||
@@ -61,7 +61,7 @@ export class HistoryManager {
|
|||||||
}
|
}
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
} else {
|
} else {
|
||||||
log.log('no changes');
|
log.log("no changes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export class HistoryManager {
|
|||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
if (this.index === -1 && this.initialState) {
|
if (this.index === -1 && this.initialState) {
|
||||||
log.log('reached start, loading initial state');
|
log.log("reached start, loading initial state");
|
||||||
return clone(this.initialState);
|
return clone(this.initialState);
|
||||||
} else {
|
} else {
|
||||||
const delta = this.history[this.index];
|
const delta = this.history[this.index];
|
||||||
@@ -95,7 +95,7 @@ export class HistoryManager {
|
|||||||
this.state = nextState;
|
this.state = nextState;
|
||||||
return clone(nextState);
|
return clone(nextState);
|
||||||
} else {
|
} else {
|
||||||
log.log('reached end');
|
log.log("reached end");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { animate, lerp } from '$lib/helpers';
|
import { animate, lerp } from "$lib/helpers";
|
||||||
import type { createKeyMap } from '$lib/helpers/createKeyMap';
|
import type { createKeyMap } from "$lib/helpers/createKeyMap";
|
||||||
import { panelState } from '$lib/sidebar/PanelState.svelte';
|
import FileSaver from "file-saver";
|
||||||
import FileSaver from 'file-saver';
|
import type { GraphManager } from "./graph-manager.svelte";
|
||||||
import type { GraphManager } from './graph-manager.svelte';
|
import type { GraphState } from "./graph-state.svelte";
|
||||||
import type { GraphState } from './graph-state.svelte';
|
|
||||||
|
|
||||||
type Keymap = ReturnType<typeof createKeyMap>;
|
type Keymap = ReturnType<typeof createKeyMap>;
|
||||||
export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) {
|
export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) {
|
||||||
|
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'l',
|
key: "l",
|
||||||
description: 'Select linked nodes',
|
description: "Select linked nodes",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const activeNode = graph.getNode(graphState.activeNodeId);
|
const activeNode = graph.getNode(graphState.activeNodeId);
|
||||||
if (activeNode) {
|
if (activeNode) {
|
||||||
@@ -19,52 +20,56 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
|||||||
graphState.selectedNodes.add(node.id);
|
graphState.selectedNodes.add(node.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: '?',
|
key: "?",
|
||||||
description: 'Toggle Help',
|
description: "Toggle Help",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
panelState.setActivePanel('shortcuts');
|
// TODO: fix this
|
||||||
}
|
// showHelp = !showHelp;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'c',
|
key: "c",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
description: 'Copy active nodes',
|
description: "Copy active nodes",
|
||||||
callback: () => graphState.copyNodes()
|
callback: () => graphState.copyNodes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'v',
|
key: "v",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
description: 'Paste nodes',
|
description: "Paste nodes",
|
||||||
callback: () => graphState.pasteNodes()
|
callback: () => graphState.pasteNodes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'Escape',
|
key: "Escape",
|
||||||
description: 'Deselect nodes',
|
description: "Deselect nodes",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
graphState.activeNodeId = -1;
|
graphState.activeNodeId = -1;
|
||||||
graphState.clearSelection();
|
graphState.clearSelection();
|
||||||
graphState.edgeEndPosition = null;
|
graphState.edgeEndPosition = null;
|
||||||
(document.activeElement as HTMLElement)?.blur();
|
(document.activeElement as HTMLElement)?.blur();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'A',
|
key: "A",
|
||||||
shift: true,
|
shift: true,
|
||||||
description: 'Add new Node',
|
description: "Add new Node",
|
||||||
callback: () => graphState.openNodePalette()
|
callback: () => {
|
||||||
|
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: '.',
|
key: ".",
|
||||||
description: 'Center camera',
|
description: "Center camera",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (!graphState.isBodyFocused()) return;
|
if (!graphState.isBodyFocused()) return;
|
||||||
|
|
||||||
@@ -85,67 +90,67 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
|||||||
animate(500, (a: number) => {
|
animate(500, (a: number) => {
|
||||||
graphState.cameraPosition[0] = lerp(camX, average[0], ease(a));
|
graphState.cameraPosition[0] = lerp(camX, average[0], ease(a));
|
||||||
graphState.cameraPosition[1] = lerp(camY, average[1], ease(a));
|
graphState.cameraPosition[1] = lerp(camY, average[1], ease(a));
|
||||||
graphState.cameraPosition[2] = lerp(camZ, 2, ease(a));
|
graphState.cameraPosition[2] = lerp(camZ, 2, ease(a))
|
||||||
if (graphState.mouseDown) return false;
|
if (graphState.mouseDown) return false;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'a',
|
key: "a",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
description: 'Select all nodes',
|
description: "Select all nodes",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (!graphState.isBodyFocused()) return;
|
if (!graphState.isBodyFocused()) return;
|
||||||
for (const node of graph.nodes.keys()) {
|
for (const node of graph.nodes.keys()) {
|
||||||
graphState.selectedNodes.add(node);
|
graphState.selectedNodes.add(node);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'z',
|
key: "z",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
description: 'Undo',
|
description: "Undo",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
if (!graphState.isBodyFocused()) return;
|
if (!graphState.isBodyFocused()) return;
|
||||||
graph.undo();
|
graph.undo();
|
||||||
for (const node of graph.nodes.values()) {
|
for (const node of graph.nodes.values()) {
|
||||||
graphState.updateNodePosition(node);
|
graphState.updateNodePosition(node);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'y',
|
key: "y",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
description: 'Redo',
|
description: "Redo",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
graph.redo();
|
graph.redo();
|
||||||
for (const node of graph.nodes.values()) {
|
for (const node of graph.nodes.values()) {
|
||||||
graphState.updateNodePosition(node);
|
graphState.updateNodePosition(node);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 's',
|
key: "s",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
description: 'Save',
|
description: "Save",
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const state = graph.serialize();
|
const state = graph.serialize();
|
||||||
const blob = new Blob([JSON.stringify(state)], {
|
const blob = new Blob([JSON.stringify(state)], {
|
||||||
type: 'application/json;charset=utf-8'
|
type: "application/json;charset=utf-8",
|
||||||
});
|
});
|
||||||
FileSaver.saveAs(blob, 'nodarium-graph.json');
|
FileSaver.saveAs(blob, "nodarium-graph.json");
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: ['Delete', 'Backspace', 'x'],
|
key: ["Delete", "Backspace", "x"],
|
||||||
description: 'Delete selected nodes',
|
description: "Delete selected nodes",
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
if (!graphState.isBodyFocused()) return;
|
if (!graphState.isBodyFocused()) return;
|
||||||
graph.startUndoGroup();
|
graph.startUndoGroup();
|
||||||
@@ -166,18 +171,20 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
|||||||
graphState.clearSelection();
|
graphState.clearSelection();
|
||||||
}
|
}
|
||||||
graph.saveUndoGroup();
|
graph.saveUndoGroup();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
keymap.addShortcut({
|
keymap.addShortcut({
|
||||||
key: 'f',
|
key: "f",
|
||||||
description: 'Smart Connect Nodes',
|
description: "Smart Connect Nodes",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const nodes = [...graphState.selectedNodes.values()]
|
const nodes = [...graphState.selectedNodes.values()]
|
||||||
.map((g) => graph.getNode(g))
|
.map((g) => graph.getNode(g))
|
||||||
.filter((n) => !!n);
|
.filter((n) => !!n);
|
||||||
const edge = graph.smartConnect(nodes[0], nodes[1]);
|
const edge = graph.smartConnect(nodes[0], nodes[1]);
|
||||||
if (!edge) graph.smartConnect(nodes[1], nodes[0]);
|
if (!edge) graph.smartConnect(nodes[1], nodes[0]);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,56 @@
|
|||||||
|
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
uniform float uWidth;
|
uniform float uWidth;
|
||||||
uniform float uHeight;
|
uniform float uHeight;
|
||||||
uniform float uZoom;
|
|
||||||
|
|
||||||
uniform vec3 uColorDark;
|
uniform vec3 uColorDark;
|
||||||
uniform vec3 uColorBright;
|
uniform vec3 uColorBright;
|
||||||
uniform vec3 uStrokeColor;
|
|
||||||
|
|
||||||
const float uHeaderHeight = 5.0;
|
uniform vec3 uStrokeColor;
|
||||||
uniform float uSectionHeights[16];
|
uniform float uStrokeWidth;
|
||||||
uniform int uNumSections;
|
|
||||||
|
|
||||||
float msign(in float x) { return (x < 0.0) ? -1.0 : 1.0; }
|
float msign(in float x) { return (x < 0.0) ? -1.0 : 1.0; }
|
||||||
float sdCircle(vec2 p, float r) { return length(p) - r; }
|
|
||||||
|
|
||||||
vec4 roundedBoxSDF( in vec2 p, in vec2 b, in float r, in float s) {
|
vec4 roundedBoxSDF( in vec2 p, in vec2 b, in float r, in float s) {
|
||||||
vec2 q = abs(p) - b + r;
|
vec2 q = abs(p) - b + r;
|
||||||
float l = b.x + b.y + 1.570796 * r;
|
float l = b.x + b.y + 1.570796 * r;
|
||||||
|
|
||||||
float k1 = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
float k1 = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
||||||
float k2 = ((q.x > 0.0) ? atan(q.y, q.x) : 1.570796);
|
float k2 = ((q.x > 0.0) ? atan(q.y, q.x) : 1.570796);
|
||||||
float k3 = 3.0 + 2.0 * msign(min(p.x, -p.y)) - msign(p.x);
|
float k3 = 3.0 + 2.0 * msign(min(p.x, -p.y)) - msign(p.x);
|
||||||
float k4 = msign(p.x * p.y);
|
float k4 = msign(p.x * p.y);
|
||||||
float k5 = r * k2 + max(-q.x, 0.0);
|
float k5 = r * k2 + max(-q.x, 0.0);
|
||||||
|
|
||||||
float ra = s * round(k1 / s);
|
float ra = s * round(k1 / s);
|
||||||
float l2 = l + 1.570796 * ra;
|
float l2 = l + 1.570796 * ra;
|
||||||
|
|
||||||
return vec4(k1 - ra, k3 * l2 + k4 * (b.y + ((q.y > 0.0) ? k5 + k2 * ra : q.y)), 4.0 * l2, k1);
|
return vec4(k1 - ra, k3 * l2 + k4 * (b.y + ((q.y > 0.0) ? k5 + k2 * ra : q.y)), 4.0 * l2, k1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main(){
|
void main(){
|
||||||
float strokeWidth = mix(2.0, 0.5, uZoom);
|
|
||||||
|
|
||||||
float borderRadius = 0.5;
|
|
||||||
float dentRadius = 0.8;
|
|
||||||
|
|
||||||
float y = (1.0 - vUv.y) * uHeight;
|
float y = (1.0-vUv.y) * uHeight;
|
||||||
float x = vUv.x * uWidth;
|
float x = vUv.x * uWidth;
|
||||||
|
|
||||||
vec2 size = vec2(uWidth, uHeight);
|
vec2 size = vec2(uWidth, uHeight);
|
||||||
vec2 uvCenter = (vUv - 0.5) * 2.0;
|
vec2 uv = (vUv - 0.5) * 2.0;
|
||||||
|
|
||||||
vec4 boxData = roundedBoxSDF(uvCenter * size, size, borderRadius * 2.0, 0.0);
|
float u_border_radius = 0.4;
|
||||||
float sceneSDF = boxData.w;
|
vec4 distance = roundedBoxSDF(uv * size, size, u_border_radius*2.0, 0.0);
|
||||||
|
|
||||||
vec2 headerDentPos = vec2(uWidth, uHeaderHeight * 0.5);
|
if (distance.w > 0.0 ) {
|
||||||
float headerDentDist = sdCircle(vec2(x, y) - headerDentPos, dentRadius);
|
// outside
|
||||||
sceneSDF = max(sceneSDF, -headerDentDist*2.0);
|
gl_FragColor = vec4(0.0,0.0,0.0, 0.0);
|
||||||
|
}else{
|
||||||
float currentYBoundary = uHeaderHeight;
|
if (distance.w > -uStrokeWidth || mod(y+5.0, 10.0) < uStrokeWidth/2.0) {
|
||||||
float previousYBoundary = uHeaderHeight;
|
// draw the outer stroke
|
||||||
|
gl_FragColor = vec4(uStrokeColor, 1.0);
|
||||||
for (int i = 0; i < 16; i++) {
|
}else if (y<5.0){
|
||||||
if (i >= uNumSections) break;
|
// draw the header
|
||||||
|
gl_FragColor = vec4(uColorBright, 1.0);
|
||||||
float sectionHeight = uSectionHeights[i];
|
}else{
|
||||||
currentYBoundary += sectionHeight;
|
gl_FragColor = vec4(uColorDark, 1.0);
|
||||||
|
}
|
||||||
float centerY = previousYBoundary + (sectionHeight * 0.5);
|
|
||||||
vec2 circlePos = vec2(0.0, centerY);
|
|
||||||
float circleDist = sdCircle(vec2(x, y) - circlePos, dentRadius);
|
|
||||||
|
|
||||||
sceneSDF = max(sceneSDF, -circleDist*2.0);
|
|
||||||
previousYBoundary = currentYBoundary;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sceneSDF > 0.05) {
|
|
||||||
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
vec3 finalColor = (y < uHeaderHeight) ? uColorBright : uColorDark;
|
|
||||||
bool isDivider = false;
|
|
||||||
|
|
||||||
float dividerY = uHeaderHeight;
|
|
||||||
if (abs(y - dividerY) < strokeWidth * 0.25) isDivider = true;
|
|
||||||
|
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
if (i >= uNumSections - 1) break;
|
|
||||||
dividerY += uSectionHeights[i];
|
|
||||||
if (abs(y - dividerY) < strokeWidth * 0.25) isDivider = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sceneSDF > -strokeWidth || isDivider) {
|
|
||||||
gl_FragColor = vec4(uStrokeColor, 1.0);
|
|
||||||
} else {
|
|
||||||
gl_FragColor = vec4(finalColor, 1.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,37 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
import type { NodeInstance } from "@nodarium/types";
|
||||||
import type { NodeInstance } from '@nodarium/types';
|
import { getGraphState } from "../graph-state.svelte";
|
||||||
import { T } from '@threlte/core';
|
import { T } from "@threlte/core";
|
||||||
import { type Mesh } from 'three';
|
import { type Mesh } from "three";
|
||||||
import { getGraphState } from '../graph-state.svelte';
|
import NodeFrag from "./Node.frag";
|
||||||
import { colors } from '../graph/colors.svelte';
|
import NodeVert from "./Node.vert";
|
||||||
import { getNodeHeight, getParameterHeight } from '../helpers/nodeHelpers';
|
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();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: NodeInstance;
|
node: NodeInstance;
|
||||||
inView: boolean;
|
inView: boolean;
|
||||||
|
z: number;
|
||||||
};
|
};
|
||||||
let { node = $bindable(), inView }: Props = $props();
|
let { node = $bindable(), inView, z }: Props = $props();
|
||||||
|
|
||||||
const nodeType = $derived(node.state.type!);
|
|
||||||
|
|
||||||
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),
|
||||||
);
|
|
||||||
|
|
||||||
const sectionHeights = $derived(
|
|
||||||
Object
|
|
||||||
.keys(nodeType.inputs || {})
|
|
||||||
.map(key => getParameterHeight(nodeType, key) / 10)
|
|
||||||
.filter(b => !!b)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let meshRef: Mesh | undefined = $state();
|
let meshRef: Mesh | undefined = $state();
|
||||||
|
|
||||||
const height = getNodeHeight(node.state.type!);
|
const height = graphState.getNodeHeight(node.type);
|
||||||
|
|
||||||
const zoom = $derived(graphState.cameraPosition[2]);
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (meshRef && !node.state?.mesh) {
|
if (meshRef && !node.state?.mesh) {
|
||||||
@@ -50,10 +39,6 @@
|
|||||||
graphState.updateNodePosition(node);
|
graphState.updateNodePosition(node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const zoomValue = $derived(
|
|
||||||
(Math.log(graphState.cameraPosition[2]) - Math.log(1)) / (Math.log(40) - Math.log(1))
|
|
||||||
);
|
|
||||||
// const zoomValue = (graphState.cameraPosition[2] - 1) / 39;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
@@ -62,7 +47,7 @@
|
|||||||
position.y={0.8}
|
position.y={0.8}
|
||||||
rotation.x={-Math.PI / 2}
|
rotation.x={-Math.PI / 2}
|
||||||
bind:ref={meshRef}
|
bind:ref={meshRef}
|
||||||
visible={inView && zoom < 7}
|
visible={inView && z < 7}
|
||||||
>
|
>
|
||||||
<T.PlaneGeometry args={[20, height]} radius={1} />
|
<T.PlaneGeometry args={[20, height]} radius={1} />
|
||||||
<T.ShaderMaterial
|
<T.ShaderMaterial
|
||||||
@@ -70,21 +55,16 @@
|
|||||||
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() },
|
||||||
uSectionHeights: { value: [5, 10] },
|
uStrokeWidth: { value: 1.0 },
|
||||||
uNumSections: { value: 2 },
|
|
||||||
uWidth: { value: 20 },
|
uWidth: { value: 20 },
|
||||||
uHeight: { value: 200 },
|
uHeight: { value: height },
|
||||||
uZoom: { value: 1.0 }
|
|
||||||
}}
|
}}
|
||||||
uniforms.uZoom.value={zoomValue}
|
uniforms.uStrokeColor.value={strokeColor.clone()}
|
||||||
uniforms.uHeight.value={height}
|
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
||||||
uniforms.uSectionHeights.value={sectionHeights}
|
|
||||||
uniforms.uNumSections.value={sectionHeights.length}
|
|
||||||
uniforms.uStrokeColor.value={strokeColor}
|
|
||||||
/>
|
/>
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|
||||||
<NodeHtml bind:node {inView} {isActive} {isSelected} z={zoom} />
|
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInstance } from '@nodarium/types';
|
import type { NodeInstance } from "@nodarium/types";
|
||||||
import { getGraphState } from '../graph-state.svelte';
|
import NodeHeader from "./NodeHeader.svelte";
|
||||||
import NodeHeader from './NodeHeader.svelte';
|
import NodeParameter from "./NodeParameter.svelte";
|
||||||
import NodeParameter from './NodeParameter.svelte';
|
import { getGraphState } from "../graph-state.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,11 +31,12 @@
|
|||||||
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[1].type !== 'seed' && !('setting' in p[1]) && p[1]?.hidden !== true
|
(p) =>
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,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}
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<NodeHeader {node} />
|
<NodeHeader {node} />
|
||||||
|
|
||||||
{#each parameters as [key, value], i (key)}
|
{#each parameters as [key, value], i}
|
||||||
<NodeParameter
|
<NodeParameter
|
||||||
bind:node
|
bind:node
|
||||||
id={key}
|
id={key}
|
||||||
@@ -71,22 +72,22 @@
|
|||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
color: var(--color-text);
|
color: var(--text-color);
|
||||||
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(--color-outline);
|
--stroke: var(--outline);
|
||||||
--stroke-width: 2px;
|
--stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node.active {
|
.node.active {
|
||||||
--stroke: var(--color-active);
|
--stroke: var(--active);
|
||||||
--stroke-width: 2px;
|
--stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node.selected {
|
.node.selected {
|
||||||
--stroke: var(--color-selected);
|
--stroke: var(--selected);
|
||||||
--stroke-width: 2px;
|
--stroke-width: 2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
import { getGraphState } from "../graph-state.svelte";
|
||||||
import type { NodeInstance, Socket } 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 { getSocketPosition } from '../helpers/nodeHelpers';
|
|
||||||
|
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
|
|
||||||
@@ -12,73 +10,47 @@
|
|||||||
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: getSocketPosition?.(node, 0)
|
position: graphState.getSocketPosition?.(node, 0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cornerTop = 10;
|
const cornerTop = 10;
|
||||||
const rightBump = $derived(!!node?.state?.type?.outputs?.length);
|
const rightBump = !!node?.state?.type?.outputs?.length;
|
||||||
const aspectRatio = 0.25;
|
const aspectRatio = 0.25;
|
||||||
|
|
||||||
const path = $derived(
|
const path = createNodePath({
|
||||||
createNodePath({
|
depth: 5.5,
|
||||||
depth: 5.5,
|
height: 34,
|
||||||
height: 34,
|
y: 49,
|
||||||
y: 49,
|
cornerTop,
|
||||||
cornerTop,
|
rightBump,
|
||||||
rightBump,
|
aspectRatio,
|
||||||
aspectRatio
|
});
|
||||||
})
|
const pathHover = createNodePath({
|
||||||
);
|
depth: 8.5,
|
||||||
const pathHover = $derived(
|
height: 50,
|
||||||
createNodePath({
|
y: 49,
|
||||||
depth: 7,
|
cornerTop,
|
||||||
height: 40,
|
rightBump,
|
||||||
y: 49,
|
aspectRatio,
|
||||||
cornerTop,
|
});
|
||||||
rightBump,
|
|
||||||
aspectRatio
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const socketId = $derived(`${node.id}-${0}`);
|
|
||||||
|
|
||||||
function getSocketType(s: Socket | null) {
|
|
||||||
if (!s) return 'unknown';
|
|
||||||
if (typeof s.index === 'string') {
|
|
||||||
return s.node.state.type?.inputs?.[s.index].type || 'unknown';
|
|
||||||
}
|
|
||||||
return s.node.state.type?.outputs?.[s.index] || 'unknown';
|
|
||||||
}
|
|
||||||
const socketType = $derived(getSocketType(graphState.activeSocket));
|
|
||||||
const hoverColor = $derived(graphState.colors.getColor(socketType));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
|
||||||
class="wrapper"
|
|
||||||
data-node-id={node.id}
|
|
||||||
data-node-type={node.type}
|
|
||||||
style:--socket-color={hoverColor}
|
|
||||||
class:possible-socket={graphState?.possibleSocketIds.has(socketId)}
|
|
||||||
>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if appSettings.value.debug.advancedMode}
|
{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="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"
|
||||||
@@ -90,7 +62,8 @@
|
|||||||
--hover-path: path("${pathHover}");
|
--hover-path: path("${pathHover}");
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"></path>
|
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,20 +74,7 @@
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.possible-socket .target::before {
|
.click-target {
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 100%;
|
|
||||||
box-shadow: 0px 0px 10px var(--socket-color);
|
|
||||||
background-color: var(--socket-color);
|
|
||||||
outline: solid thin var(--socket-color);
|
|
||||||
opacity: 0.7;
|
|
||||||
z-index: -10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -123,9 +83,11 @@
|
|||||||
width: 30px;
|
width: 30px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
/* background: red; */
|
||||||
|
/* opacity: 0.2; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.target:hover + svg path {
|
.click-target:hover + svg path {
|
||||||
d: var(--hover-path);
|
d: var(--hover-path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +107,10 @@
|
|||||||
transition:
|
transition:
|
||||||
d 0.3s ease,
|
d 0.3s ease,
|
||||||
fill 0.3s ease;
|
fill 0.3s ease;
|
||||||
fill: var(--color-layer-2);
|
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 { NodeInput, NodeInstance } from '@nodarium/types';
|
import type { NodeInstance, NodeInput } 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,39 +16,25 @@
|
|||||||
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 === 'boolean') return 0;
|
if (input.type === "float") return 0.5;
|
||||||
if (input.type === 'float') return 0.5;
|
if (input.type === "integer") return 0;
|
||||||
if (input.type === 'integer') return 0;
|
if (input.type === "select") return 0;
|
||||||
if (input.type === 'select') return 0;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = $state(structuredClone($state.snapshot(getDefaultValue())));
|
let value = $state(getDefaultValue());
|
||||||
|
|
||||||
function diffArray(a: number[], b?: number[] | number) {
|
|
||||||
if (!Array.isArray(b)) return true;
|
|
||||||
if (Array.isArray(a) !== Array.isArray(b)) return true;
|
|
||||||
if (a.length !== b.length) return true;
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
|
||||||
if (a[i] !== b[i]) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const a = $state.snapshot(value);
|
if (value !== undefined && node?.props?.[id] !== value) {
|
||||||
const b = $state.snapshot(node?.props?.[id]);
|
node.props = { ...node.props, [id]: value };
|
||||||
const isDiff = Array.isArray(a) ? diffArray(a, b) : a !== b;
|
|
||||||
if (value !== undefined && isDiff) {
|
|
||||||
node.props = { ...node.props, [id]: a };
|
|
||||||
if (graph) {
|
if (graph) {
|
||||||
graph.save();
|
graph.save();
|
||||||
graph.execute();
|
graph.execute();
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInput, NodeInstance, Socket } from '@nodarium/types';
|
import type { NodeInput, NodeInstance } from "@nodarium/types";
|
||||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
import { createNodePath } from "../helpers";
|
||||||
import { createNodePath } from '../helpers';
|
import NodeInputEl from "./NodeInput.svelte";
|
||||||
import { getParameterHeight, getSocketPosition } from '../helpers/nodeHelpers';
|
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
||||||
import NodeInputEl from './NodeInput.svelte';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: NodeInstance;
|
node: NodeInstance;
|
||||||
@@ -13,18 +12,17 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const graph = getGraphManager();
|
const graph = getGraphManager();
|
||||||
const graphState = getGraphState();
|
|
||||||
const graphId = graph?.id;
|
|
||||||
const elementId = `input-${Math.random().toString(36).substring(7)}`;
|
|
||||||
|
|
||||||
let { node = $bindable(), input, id, isLast }: Props = $props();
|
let { node = $bindable(), input, id, isLast }: Props = $props();
|
||||||
|
|
||||||
const nodeType = $derived(node.state.type!);
|
const inputType = node?.state?.type?.inputs?.[id]!;
|
||||||
|
|
||||||
const inputType = $derived(nodeType.inputs?.[id]);
|
const socketId = `${node.id}-${id}`;
|
||||||
|
|
||||||
const socketId = $derived(`${node.id}-${id}`);
|
const graphState = getGraphState();
|
||||||
const height = $derived(getParameterHeight(nodeType, id));
|
const graphId = graph?.id;
|
||||||
|
|
||||||
|
const elementId = `input-${Math.random().toString(36).substring(7)}`;
|
||||||
|
|
||||||
function handleMouseDown(ev: MouseEvent) {
|
function handleMouseDown(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@@ -32,83 +30,79 @@
|
|||||||
graphState.setDownSocket({
|
graphState.setDownSocket({
|
||||||
node,
|
node,
|
||||||
index: id,
|
index: id,
|
||||||
position: getSocketPosition(node, id)
|
position: graphState.getSocketPosition?.(node, id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const leftBump = $derived(nodeType.inputs?.[id].internal !== true);
|
const leftBump = node.state?.type?.inputs?.[id].internal !== true;
|
||||||
const cornerBottom = $derived(isLast ? 5 : 0);
|
const cornerBottom = isLast ? 5 : 0;
|
||||||
const aspectRatio = 0.5;
|
const aspectRatio = 0.5;
|
||||||
|
|
||||||
const path = $derived(
|
const path = createNodePath({
|
||||||
createNodePath({
|
depth: 7,
|
||||||
depth: 6,
|
height: 20,
|
||||||
height: 2000 / height,
|
y: 50.5,
|
||||||
y: 50.5,
|
cornerBottom,
|
||||||
cornerBottom,
|
leftBump,
|
||||||
leftBump,
|
aspectRatio,
|
||||||
aspectRatio
|
});
|
||||||
})
|
const pathDisabled = createNodePath({
|
||||||
);
|
depth: 6,
|
||||||
const pathHover = $derived(
|
height: 18,
|
||||||
createNodePath({
|
y: 50.5,
|
||||||
depth: 7,
|
cornerBottom,
|
||||||
height: 2200 / height,
|
leftBump,
|
||||||
y: 50.5,
|
aspectRatio,
|
||||||
cornerBottom,
|
});
|
||||||
leftBump,
|
const pathHover = createNodePath({
|
||||||
aspectRatio
|
depth: 8,
|
||||||
})
|
height: 25,
|
||||||
);
|
y: 50.5,
|
||||||
|
cornerBottom,
|
||||||
function getSocketType(s: Socket | null) {
|
leftBump,
|
||||||
if (!s) return 'unknown';
|
aspectRatio,
|
||||||
if (typeof s.index === 'string') {
|
});
|
||||||
return s.node.state.type?.inputs?.[s.index].type || 'unknown';
|
|
||||||
}
|
|
||||||
return s.node.state.type?.outputs?.[s.index] || 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
const socketType = $derived(getSocketType(graphState.activeSocket));
|
|
||||||
const hoverColor = $derived(graphState.colors.getColor(socketType));
|
|
||||||
</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}
|
||||||
style:height="{height}px"
|
class:disabled={!graphState?.possibleSocketIds.has(socketId)}
|
||||||
style:--socket-color={hoverColor}
|
|
||||||
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}>{input.label || id}</label>
|
<label for={elementId}>{input.label || id}</label>
|
||||||
{/if}
|
{/if}
|
||||||
{#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="target"
|
class="small target"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
></div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
<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"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
style:--path={`path("${path}")`}
|
style={`
|
||||||
style:--hover-path={`path("${pathHover}")`}
|
--path: path("${path}");
|
||||||
|
--hover-path: path("${pathHover}");
|
||||||
|
--hover-path-disabled: path("${pathDisabled}");
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<path vector-effect="non-scaling-stroke"></path>
|
<path vector-effect="non-scaling-stroke"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -118,43 +112,42 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
transform: translateY(-0.5px);
|
transform: translateY(-0.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.possible-socket .target::before {
|
.small.target {
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 100%;
|
|
||||||
box-shadow: 0px 0px 10px var(--socket-color);
|
|
||||||
background-color: var(--socket-color);
|
|
||||||
outline: solid thin var(--socket-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
z-index: -10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.target:hover ~ svg path{
|
.large.target {
|
||||||
d: var(--hover-path);
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
cursor: unset;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.hovering-sockets) .large.target {
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 10px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-inline: 20px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: center;
|
justify-content: space-around;
|
||||||
gap: 10px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,21 +163,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg path {
|
svg path {
|
||||||
transition: d 0.3s ease, fill 0.3s ease;
|
transition:
|
||||||
fill: var(--color-layer-1);
|
d 0.3s ease,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
stroke-linejoin: round;
|
:global {
|
||||||
shape-rendering: geometricPrecision;
|
.hovering-sockets .large:hover ~ svg path {
|
||||||
|
d: var(--hover-path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content.disabled {
|
.content.disabled {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
.content.disabled > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.possible-socket svg path {
|
.disabled svg path {
|
||||||
d: var(--hover-path);
|
d: var(--hover-path-disabled) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
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()
|
|
||||||
};
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
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,95 +1 @@
|
|||||||
{
|
{"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,10 +1,11 @@
|
|||||||
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;
|
||||||
@@ -17,18 +18,19 @@ 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 { default as defaultPlant } from './default.json';
|
export { grid } from "./grid";
|
||||||
export { grid } from './grid';
|
export { tree } from "./tree";
|
||||||
export { default as lottaFaces } from './lotta-faces.json';
|
export { plant } from "./plant";
|
||||||
export { default as lottaNodesAndFaces } from './lotta-nodes-and-faces.json';
|
export { default as lottaFaces } from "./lotta-faces.json";
|
||||||
export { default as lottaNodes } from './lotta-nodes.json';
|
export { default as lottaNodes } from "./lotta-nodes.json";
|
||||||
export { plant } from './plant';
|
export { default as defaultPlant } from "./default.json"
|
||||||
export { default as simple } from './simple.json';
|
export { default as lottaNodesAndFaces } from "./lotta-nodes-and-faces.json";
|
||||||
export { tree } from './tree';
|
|
||||||
|
|||||||
@@ -1,44 +1 @@
|
|||||||
{
|
{"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,71 +1,12 @@
|
|||||||
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': 10,
|
{ "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 } },
|
||||||
'position': [55, 80],
|
{ "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 } },
|
||||||
'type': 'max/plantarium/stem',
|
{ "id": 13, "position": [130, 80], "type": "max/plantarium/noise", "props": { "strength": 8, "scale": 7.7, "fixBottom": 1, "directionalStrength": [1, 0, 1], "depth": 1 } },
|
||||||
'props': { 'amount': 1, 'length': 11, 'thickness': 0.71 }
|
{ "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 } }
|
||||||
},
|
|
||||||
{
|
|
||||||
'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': [
|
"edges": [[10, 0, 11, "plant"], [11, 0, 12, "plant"], [12, 0, 13, "plant"], [13, 0, 14, "plant"], [14, 0, 9, "input"]]
|
||||||
[10, 0, 11, 'plant'],
|
}
|
||||||
[11, 0, 12, 'plant'],
|
|
||||||
[12, 0, 13, 'plant'],
|
|
||||||
[13, 0, 14, 'plant'],
|
|
||||||
[14, 0, 9, 'input']
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"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,26 +1,28 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -31,22 +33,24 @@ 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,24 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, type Snippet } from 'svelte';
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
let index = $state(-1);
|
let index = -1;
|
||||||
let wrapper: HTMLDivElement;
|
let wrapper: HTMLDivElement;
|
||||||
|
|
||||||
const { children } = $props<{ children?: Snippet }>();
|
$: if (index === -1) {
|
||||||
|
index = getContext<() => number>("registerCell")();
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
const sizes = getContext<{ value: string[] }>("sizes");
|
||||||
if (index === -1) {
|
|
||||||
index = getContext<() => number>('registerCell')();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -33,8 +31,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
onmouseup={() => (mouseDown = false)}
|
on:mouseup={() => (mouseDown = false)}
|
||||||
onmousemove={handleMouseMove}
|
on:mousemove={handleMouseMove}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if index > 0}
|
{#if index > 0}
|
||||||
@@ -42,13 +40,12 @@
|
|||||||
class="seperator"
|
class="seperator"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onmousedown={handleMouseDown}
|
on:mousedown={handleMouseDown}
|
||||||
>
|
></div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="cell" bind:this={wrapper}>
|
<div class="cell" bind:this={wrapper}>
|
||||||
{@render children?.()}
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -62,7 +59,7 @@
|
|||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: var(--color-outline);
|
background: var(--outline);
|
||||||
}
|
}
|
||||||
.seperator::before {
|
.seperator::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, setContext, type Snippet } from 'svelte';
|
import { setContext } from "svelte";
|
||||||
|
|
||||||
const { children, id } = $props<{ children?: Snippet; id?: string }>();
|
export let id = "grid-0";
|
||||||
|
|
||||||
onMount(() => {
|
setContext("grid-id", id);
|
||||||
setContext('grid-id', id);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children({ id })}
|
<slot {id} />
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { localState } from '$lib/helpers/localState.svelte';
|
import { setContext, getContext } from "svelte";
|
||||||
import { getContext, setContext } from 'svelte';
|
import { localState } from "$lib/helpers/localState.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 Cell from './Cell.svelte';
|
import Grid from "./Grid.svelte";
|
||||||
import Grid from './Grid.svelte';
|
import Row from "./Row.svelte";
|
||||||
import Row from './Row.svelte';
|
import Cell from "./Cell.svelte";
|
||||||
|
|
||||||
export default withSubComponents(Grid, { Row, Cell });
|
export default withSubComponents(Grid, { Row, Cell });
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
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,39 +1,38 @@
|
|||||||
import { derived, get, writable } from 'svelte/store';
|
import { derived, get, writable } from "svelte/store";
|
||||||
|
|
||||||
export type ShortCut = {
|
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' : ''
|
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createKeyMap(keys: ShortCut[]) {
|
function getShortcutId(shortcut: 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 => {
|
||||||
@@ -53,5 +52,6 @@ export function createKeyMap(keys: ShortCut[]) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keys: derived(store, $store => Array.from($store.values()))
|
keys: derived(store, $store => Array.from($store.values()))
|
||||||
};
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
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,11 +30,10 @@ 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}
|
||||||
@@ -43,45 +42,40 @@ 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
|
||||||
leftBump
|
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
||||||
? ` 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: () => void, ms = 300) => {
|
export const debounce = (fn: Function, ms = 300) => {
|
||||||
let timeoutId: ReturnType<typeof setTimeout>;
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
return function(this: unknown, ...args: unknown[]) {
|
return function (this: any, ...args: any[]) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
timeoutId = setTimeout(() => fn.apply(this, args as []), ms);
|
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 : (obj) => JSON.parse(JSON.stringify(obj));
|
||||||
? 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 {
|
||||||
@@ -93,7 +87,7 @@ export function withSubComponents<A, B extends Record<string, unknown>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
@@ -110,15 +104,11 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
|||||||
const millisecondsPerHour = 3600000;
|
const millisecondsPerHour = 3600000;
|
||||||
const millisecondsPerDay = 86400000;
|
const millisecondsPerDay = 86400000;
|
||||||
|
|
||||||
const days = Math.floor(durationInMilliseconds / millisecondsPerDay);
|
let days = Math.floor(durationInMilliseconds / millisecondsPerDay);
|
||||||
const hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
|
let hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
|
||||||
const minutes = Math.floor(
|
let minutes = Math.floor((durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute);
|
||||||
(durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute
|
let seconds = Math.floor((durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond);
|
||||||
);
|
let millis = durationInMilliseconds % millisecondsPerSecond;
|
||||||
const seconds = Math.floor(
|
|
||||||
(durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond
|
|
||||||
);
|
|
||||||
const millis = durationInMilliseconds % millisecondsPerSecond;
|
|
||||||
|
|
||||||
let durationString = '';
|
let durationString = '';
|
||||||
|
|
||||||
@@ -141,10 +131,32 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
|||||||
|
|
||||||
return durationString.trim();
|
return durationString.trim();
|
||||||
}
|
}
|
||||||
|
// export function debounceAsyncFunction<T extends any[], R>(
|
||||||
export function debounceAsyncFunction<T extends (...args: never[]) => Promise<unknown>>(
|
// func: (...args: T) => Promise<R>
|
||||||
asyncFn: T
|
// ): (...args: T) => Promise<R> {
|
||||||
): T {
|
// let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
// let lastPromise: Promise<R> | null = null;
|
||||||
|
// let lastReject: ((reason?: any) => void) | null = null;
|
||||||
|
//
|
||||||
|
// return (...args: T): Promise<R> => {
|
||||||
|
// if (timeoutId) {
|
||||||
|
// clearTimeout(timeoutId);
|
||||||
|
// if (lastReject) {
|
||||||
|
// lastReject(new Error("Debounced: Previous call was canceled."));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return new Promise<R>((resolve, reject) => {
|
||||||
|
// lastReject = reject;
|
||||||
|
// timeoutId = setTimeout(() => {
|
||||||
|
// timeoutId = null;
|
||||||
|
// lastReject = null;
|
||||||
|
// lastPromise = func(...args).then(resolve, reject);
|
||||||
|
// }, 300); // Default debounce time is 300ms; you can make this configurable.
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>>(asyncFn: T): T {
|
||||||
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;
|
||||||
@@ -165,7 +177,7 @@ export function debounceAsyncFunction<T extends (...args: never[]) => Promise<un
|
|||||||
try {
|
try {
|
||||||
// Execute with the latest arguments
|
// Execute with the latest arguments
|
||||||
const result = await asyncFn(...latestArgs!);
|
const result = await asyncFn(...latestArgs!);
|
||||||
return result as ReturnType<T>;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
// Allow the next execution
|
// Allow the next execution
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
@@ -178,18 +190,48 @@ export function debounceAsyncFunction<T extends (...args: never[]) => Promise<un
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withArgsChangeOnly<T extends unknown[], R>(
|
// export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
|
||||||
func: (...args: T) => R
|
// let currentPromise: Promise<R> | null = null;
|
||||||
): (...args: T) => R {
|
// let nextArgs: T | null = null;
|
||||||
|
// let resolveNext: ((result: R) => void) | null = null;
|
||||||
|
//
|
||||||
|
// const debouncedFunction = async (...args: T): Promise<R> => {
|
||||||
|
// if (currentPromise) {
|
||||||
|
// // Store the latest arguments and create a new promise to resolve them later
|
||||||
|
// nextArgs = args;
|
||||||
|
// return new Promise<R>((resolve) => {
|
||||||
|
// resolveNext = resolve;
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// // Execute the function immediately
|
||||||
|
// try {
|
||||||
|
// currentPromise = func(...args);
|
||||||
|
// const result = await currentPromise;
|
||||||
|
// return result;
|
||||||
|
// } finally {
|
||||||
|
// currentPromise = null;
|
||||||
|
// // If there are stored arguments, call the function again with the latest arguments
|
||||||
|
// if (nextArgs) {
|
||||||
|
// const argsToUse = nextArgs;
|
||||||
|
// const resolver = resolveNext;
|
||||||
|
// nextArgs = null;
|
||||||
|
// resolveNext = null;
|
||||||
|
// resolver!(await debouncedFunction(...argsToUse));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// return debouncedFunction;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
|
||||||
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 (
|
if (lastArgs && args.length === lastArgs.length && args.every((val, index) => val === lastArgs?.[index])) {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,3 +241,4 @@ export function withArgsChangeOnly<T extends unknown[], R>(
|
|||||||
return lastResult; // Return new result
|
return lastResult; // Return new result
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,8 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
function mergeRecursive<T>(current: T, initial: T): T {
|
|
||||||
if (typeof initial === 'number') {
|
|
||||||
if (typeof current === 'number') return current;
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof initial === 'boolean') {
|
|
||||||
if (typeof current === 'boolean') return current;
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(initial)) {
|
|
||||||
if (Array.isArray(current)) return current;
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof initial === 'object' && initial) {
|
|
||||||
const merged = initial;
|
|
||||||
if (typeof current === 'object' && current) {
|
|
||||||
for (const key of Object.keys(initial)) {
|
|
||||||
if (key in current) {
|
|
||||||
// @ts-expect-error It's safe dont worry about it
|
|
||||||
merged[key] = mergeRecursive(current[key], initial[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -42,10 +10,7 @@ export class LocalStore<T> {
|
|||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const item = localStorage.getItem(key);
|
const item = localStorage.getItem(key);
|
||||||
if (item) {
|
if (item) this.value = this.deserialize(item);
|
||||||
const storedValue = this.deserialize(item);
|
|
||||||
this.value = mergeRecursive(storedValue, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { type Writable, writable } from 'svelte/store';
|
import { writable, type 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) {
|
||||||
@@ -35,15 +36,18 @@ 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,10 +1,6 @@
|
|||||||
<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
|
<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"/>
|
||||||
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: 509 B After Width: | Height: | Size: 496 B |
@@ -1,14 +1,13 @@
|
|||||||
import { createWasmWrapper } from '@nodarium/utils';
|
import { createWasmWrapper } from "@nodarium/utils";
|
||||||
import fs from 'fs/promises';
|
import fs from "fs/promises";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
|
|
||||||
export async function getWasm(id: `${string}/${string}/${string}`) {
|
export async function getWasm(id: `${string}/${string}/${string}`) {
|
||||||
const filePath = path.resolve(`./static/nodes/${id}`);
|
const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`);
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +20,9 @@ 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;
|
||||||
|
|
||||||
try {
|
const wrapper = createWasmWrapper(wasmBytes);
|
||||||
return createWasmWrapper(wasmBytes.buffer);
|
|
||||||
} catch (error) {
|
return wrapper;
|
||||||
console.error(`Failed to create node wrapper for node: ${id}`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNode(id: `${string}/${string}/${string}`) {
|
export async function getNode(id: `${string}/${string}/${string}`) {
|
||||||
@@ -39,12 +36,12 @@ export async function getNode(id: `${string}/${string}/${string}`) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getCollectionNodes(userId: `${string}/${string}`) {
|
export async function getCollectionNodes(userId: `${string}/${string}`) {
|
||||||
const nodes = await fs.readdir(path.resolve(`./static/nodes/${userId}`));
|
const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`));
|
||||||
return nodes
|
return nodes
|
||||||
.filter((n) => n !== 'pkg' && n !== '.template')
|
.filter((n) => n !== "pkg" && n !== ".template")
|
||||||
.map((n) => {
|
.map((n) => {
|
||||||
return {
|
return {
|
||||||
id: `${userId}/${n}`
|
id: `${userId}/${n}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -53,20 +50,20 @@ export async function getCollection(userId: `${string}/${string}`) {
|
|||||||
const nodes = await getCollectionNodes(userId);
|
const nodes = await getCollectionNodes(userId);
|
||||||
return {
|
return {
|
||||||
id: userId,
|
id: userId,
|
||||||
nodes
|
nodes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserCollections(userId: string) {
|
export async function getUserCollections(userId: string) {
|
||||||
const collections = await fs.readdir(path.resolve(`./static/nodes/${userId}`));
|
const collections = await fs.readdir(path.resolve(`../nodes/${userId}`));
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
collections.map(async (n) => {
|
collections.map(async (n) => {
|
||||||
const nodes = await getCollectionNodes(`${userId}/${n}`);
|
const nodes = await getCollectionNodes(`${userId}/${n}`);
|
||||||
return {
|
return {
|
||||||
id: `${userId}/${n}`,
|
id: `${userId}/${n}`,
|
||||||
nodes
|
nodes,
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,20 +71,20 @@ export async function getUser(userId: string) {
|
|||||||
const collections = await getUserCollections(userId);
|
const collections = await getUserCollections(userId);
|
||||||
return {
|
return {
|
||||||
id: userId,
|
id: userId,
|
||||||
collections
|
collections,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUsers() {
|
export async function getUsers() {
|
||||||
const nodes = await fs.readdir(path.resolve('./static/nodes'));
|
const nodes = await fs.readdir(path.resolve("../nodes"));
|
||||||
const users = await Promise.all(
|
const users = await Promise.all(
|
||||||
nodes.map(async (n) => {
|
nodes.map(async (n) => {
|
||||||
const collections = await getUserCollections(n);
|
const collections = await getUserCollections(n);
|
||||||
return {
|
return {
|
||||||
id: n,
|
id: n,
|
||||||
collections
|
collections,
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export const debugNode = {
|
|
||||||
id: 'max/plantarium/debug',
|
|
||||||
inputs: {
|
|
||||||
input: {
|
|
||||||
type: '*'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
execute(_data: Int32Array): Int32Array {
|
|
||||||
return _data;
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './node-registry-cache';
|
|
||||||
export * from './node-registry-client';
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user