Compare commits
54 Commits
v0.0.4
...
feat/arena
| Author | SHA1 | Date | |
|---|---|---|---|
|
be8161ec8d
|
|||
|
4cb24e8ff9
|
|||
|
|
30afb30341
|
||
|
|
1d1a44324e
|
||
|
|
343eca02b5
|
||
|
|
45a9800e6a
|
||
|
|
b384348e70
|
||
|
|
25ceb6e94f
|
||
|
|
ff8c6637f8
|
||
|
2ec9bfc3c9
|
|||
|
c97520617a
|
|||
|
6475790176
|
|||
|
580ec73465
|
|||
|
fd98d457a3
|
|||
|
f16ba2601f
|
|||
|
cc6b832f15
|
|||
|
dd5fd5bf17
|
|||
|
38d0fffcf4
|
|||
|
bce06da456
|
|||
|
af585d56ec
|
|||
|
0aa73a27c1
|
|||
|
c1ae70282c
|
|||
|
4c7b03dfb8
|
|||
|
144e8cc797
|
|||
| 12ff9c1518 | |||
| 8d3ffe84ab | |||
|
95ec93eead
|
|||
|
d39185efaf
|
|||
|
81580ccd8c
|
|||
|
bf6f632d27
|
|||
|
|
e098be6013
|
||
|
|
ec13850e1c
|
||
|
|
15e08a8163
|
||
|
|
48cee58ad3
|
||
|
|
3235cae904
|
||
|
|
3f440728fc
|
||
|
|
da09f8ba1e
|
||
|
|
ddc3b4ce35
|
||
|
|
2690fc8712
|
||
|
|
072ab9063b
|
||
|
|
e23cad254d
|
||
|
|
5b5c63c1a9
|
||
|
|
c9021f2383
|
||
| 9eecdd4fb8 | |||
|
|
7e71a41e52
|
||
|
|
07cd9e84eb
|
||
|
|
a31a49ad50
|
||
|
|
850d641a25
|
||
|
|
ee5ca81757
|
||
|
|
22a11832b8
|
||
|
|
b5ce5723fa
|
||
|
|
102130cc77
|
||
|
|
1668a2e6d5
|
||
|
|
b0af83004e |
9
.cargo/config.toml
Normal file
9
.cargo/config.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"link-arg=--import-memory",
|
||||
"-C",
|
||||
"link-arg=--initial-memory=67108864", # 64 MiB
|
||||
"-C",
|
||||
"link-arg=--max-memory=536870912", # 512 MiB
|
||||
]
|
||||
@@ -58,6 +58,7 @@ tmp_changelog="CHANGELOG.tmp"
|
||||
echo ""
|
||||
if [ -n "$COMMITS" ]; then
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "$COMMITS"
|
||||
echo ""
|
||||
fi
|
||||
@@ -72,12 +73,24 @@ mv "$tmp_changelog" CHANGELOG.md
|
||||
pnpm exec dprint fmt CHANGELOG.md
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 5. Create release commit
|
||||
# 5. Setup GPG signing
|
||||
# -------------------------------------------------------------------
|
||||
git config user.name "release-bot"
|
||||
git config user.email "release-bot@ci"
|
||||
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)
|
||||
|
||||
git add CHANGELOG.md $(find . -name package.json ! -path "*/node_modules/*")
|
||||
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"
|
||||
|
||||
59
.gitea/workflows/benchmark.yaml
Normal file
59
.gitea/workflows/benchmark.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
||||
@@ -15,7 +15,7 @@ env:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.max-richter.dev/max/nodarium-ci:fd7268d6208aede435e1685817ae6b271c68bd83
|
||||
container: git.max-richter.dev/max/nodarium-ci:bce06da456e3c008851ac006033cfff256015a47
|
||||
|
||||
steps:
|
||||
- name: 📑 Checkout Code
|
||||
@@ -48,15 +48,17 @@ jobs:
|
||||
|
||||
- name: 🧹 Quality Control
|
||||
run: |
|
||||
pnpm build
|
||||
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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@ node_modules/
|
||||
|
||||
/target
|
||||
.direnv/
|
||||
|
||||
.pnpm-store/
|
||||
|
||||
128
CHANGELOG.md
128
CHANGELOG.md
@@ -1,3 +1,131 @@
|
||||
# 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
|
||||
|
||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -24,6 +24,14 @@ dependencies = [
|
||||
"nodarium_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debug"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nodarium_macros",
|
||||
"nodarium_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -6,6 +6,8 @@ ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
|
||||
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 \
|
||||
|
||||
@@ -27,7 +27,6 @@ Currently this visual programming language is used to develop <https://nodes.max
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- [pnpm](https://pnpm.io/installation)
|
||||
- [rust](https://www.rust-lang.org/tools/install)
|
||||
- wasm-pack
|
||||
|
||||
### Install dependencies
|
||||
|
||||
|
||||
783
SHARED_MEMORY_REFACTOR_PLAN.md
Normal file
783
SHARED_MEMORY_REFACTOR_PLAN.md
Normal file
@@ -0,0 +1,783 @@
|
||||
# Shared Memory Refactor Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Migrate to a single shared `WebAssembly.Memory` instance imported by all nodes using `--import-memory`. The `#[nodarium_execute]` macro writes the function's return value directly to shared memory at the specified offset.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Shared WebAssembly.Memory │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [Node A output] [Node B output] [Node C output] ... │ │
|
||||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||||
│ │ │ Vec<i32> │ │ Vec<i32> │ │ Vec<i32> │ │ │
|
||||
│ │ │ 4 bytes │ │ 12 bytes │ │ 2KB │ │ │
|
||||
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ offset: 0 ────────────────────────────────────────────────► │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
│ import { memory } from "env"
|
||||
┌─────────────────────────┼─────────────────────────┐
|
||||
│ │ │
|
||||
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
|
||||
│ Node A │ │ Node B │ │ Node C │
|
||||
│ WASM │ │ WASM │ │ WASM │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
## Phase 1: Compilation Configuration
|
||||
|
||||
### 1.1 Cargo Config
|
||||
|
||||
```toml
|
||||
# nodes/max/plantarium/box/.cargo/config.toml
|
||||
[build]
|
||||
rustflags = ["-C", "link-arg=--import-memory"]
|
||||
```
|
||||
|
||||
Or globally in `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.release]
|
||||
rustflags = ["-C", "link-arg=--import-memory"]
|
||||
```
|
||||
|
||||
### 1.2 Import Memory Semantics
|
||||
|
||||
With `--import-memory`:
|
||||
|
||||
- Nodes **import** memory from the host (not export their own)
|
||||
- All nodes receive the same `WebAssembly.Memory` instance
|
||||
- Memory is read/write accessible from all modules
|
||||
- No `memory.grow` needed (host manages allocation)
|
||||
|
||||
## Phase 2: Macro Design
|
||||
|
||||
### 2.1 Clean Node API
|
||||
|
||||
```rust
|
||||
// input.json has 3 inputs: op_type, a, b
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(op_type: *i32, a: *i32, b: *i32) -> Vec<i32> {
|
||||
// Read inputs directly from shared memory
|
||||
let op = unsafe { *op_type };
|
||||
let a_val = f32::from_bits(unsafe { *a } as u32);
|
||||
let b_val = f32::from_bits(unsafe { *b } as u32);
|
||||
|
||||
let result = match op {
|
||||
0 => a_val + b_val,
|
||||
1 => a_val - b_val,
|
||||
2 => a_val * b_val,
|
||||
3 => a_val / b_val,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Return Vec<i32>, macro handles writing to shared memory
|
||||
vec![result.to_bits()]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Macro Implementation
|
||||
|
||||
```rust
|
||||
// packages/macros/src/lib.rs
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn nodarium_execute(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input_fn = parse_macro_input!(item as syn::ItemFn);
|
||||
let fn_name = &input_fn.sig.ident;
|
||||
|
||||
// Parse definition to get input count
|
||||
let project_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let def: NodeDefinition = serde_json::from_str(&fs::read_to_string(
|
||||
Path::new(&project_dir).join("src/input.json")
|
||||
).unwrap()).unwrap();
|
||||
|
||||
let input_count = def.inputs.as_ref().map(|i| i.len()).unwrap_or(0);
|
||||
|
||||
// Validate signature
|
||||
validate_signature(&input_fn, input_count);
|
||||
|
||||
// Generate wrapper
|
||||
generate_execute_wrapper(input_fn, fn_name, input_count)
|
||||
}
|
||||
|
||||
fn validate_signature(fn_sig: &syn::Signature, expected_inputs: usize) {
|
||||
let param_count = fn_sig.inputs.len();
|
||||
if param_count != expected_inputs {
|
||||
panic!(
|
||||
"Execute function has {} parameters but definition has {} inputs\n\
|
||||
Definition inputs: {:?}\n\
|
||||
Expected signature:\n\
|
||||
pub fn execute({}) -> Vec<i32>",
|
||||
param_count,
|
||||
expected_inputs,
|
||||
def.inputs.as_ref().map(|i| i.keys().collect::<Vec<_>>()),
|
||||
(0..expected_inputs)
|
||||
.map(|i| format!("arg{}: *const i32", i))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
// Verify return type is Vec<i32>
|
||||
match &fn_sig.output {
|
||||
syn::ReturnType::Type(_, ty) => {
|
||||
if !matches!(&**ty, syn::Type::Path(tp) if tp.path.is_ident("Vec")) {
|
||||
panic!("Execute function must return Vec<i32>");
|
||||
}
|
||||
}
|
||||
syn::ReturnType::Default => {
|
||||
panic!("Execute function must return Vec<i32>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_execute_wrapper(
|
||||
input_fn: syn::ItemFn,
|
||||
fn_name: &syn::Ident,
|
||||
input_count: usize,
|
||||
) -> TokenStream {
|
||||
let arg_names: Vec<_> = (0..input_count)
|
||||
.map(|i| syn::Ident::new(&format!("arg{}", i), proc_macro2::Span::call_site()))
|
||||
.collect();
|
||||
|
||||
let expanded = quote! {
|
||||
#input_fn
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn execute(
|
||||
output_pos: i32,
|
||||
#( #arg_names: i32 ),*
|
||||
) -> i32 {
|
||||
extern "C" {
|
||||
fn __nodarium_log(ptr: *const u8, len: usize);
|
||||
fn __nodarium_log_panic(ptr: *const u8, len: usize);
|
||||
}
|
||||
|
||||
// Setup panic hook
|
||||
static SET_HOOK: std::sync::Once = std::sync::Once::new();
|
||||
SET_HOOK.call_once(|| {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let msg = info.to_string();
|
||||
unsafe { __nodarium_log_panic(msg.as_ptr(), msg.len()); }
|
||||
}));
|
||||
});
|
||||
|
||||
// Call user function
|
||||
let result = #fn_name(
|
||||
#( #arg_names as *const i32 ),*
|
||||
);
|
||||
|
||||
// Write result directly to shared memory at output_pos
|
||||
let len_bytes = result.len() * 4;
|
||||
unsafe {
|
||||
let src = result.as_ptr() as *const u8;
|
||||
let dst = output_pos as *mut u8;
|
||||
dst.copy_from_nonoverlapping(src, len_bytes);
|
||||
}
|
||||
|
||||
// Forget the Vec to prevent deallocation (data is in shared memory now)
|
||||
core::mem::forget(result);
|
||||
|
||||
len_bytes as i32
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Generated Assembly
|
||||
|
||||
The macro generates:
|
||||
|
||||
```asm
|
||||
; Input: output_pos in register r0, arg0 in r1, arg1 in r2, arg2 in r3
|
||||
execute:
|
||||
; Call user function
|
||||
bl user_execute ; returns pointer to Vec<i32> in r0
|
||||
|
||||
; Calculate byte length
|
||||
ldr r4, [r0, #8] ; Vec::len field
|
||||
lsl r4, r4, #2 ; len * 4 (i32 = 4 bytes)
|
||||
|
||||
; Copy Vec data to shared memory at output_pos
|
||||
ldr r5, [r0, #0] ; Vec::ptr field
|
||||
ldr r6, [r0, #4] ; capacity (unused)
|
||||
|
||||
; memcpy(dst=output_pos, src=r5, len=r4)
|
||||
; (implemented via copy_from_nonoverlapping)
|
||||
|
||||
; Return length
|
||||
mov r0, r4
|
||||
bx lr
|
||||
```
|
||||
|
||||
## Phase 3: Input Reading Helpers
|
||||
|
||||
```rust
|
||||
// packages/utils/src/accessor.rs
|
||||
|
||||
/// Read i32 from shared memory
|
||||
#[inline]
|
||||
pub unsafe fn read_i32(ptr: *const i32) -> i32 {
|
||||
*ptr
|
||||
}
|
||||
|
||||
/// Read f32 from shared memory (stored as i32 bits)
|
||||
#[inline]
|
||||
pub unsafe fn read_f32(ptr: *const i32) -> f32 {
|
||||
f32::from_bits(*ptr as u32)
|
||||
}
|
||||
|
||||
/// Read boolean from shared memory
|
||||
#[inline]
|
||||
pub unsafe fn read_bool(ptr: *const i32) -> bool {
|
||||
*ptr != 0
|
||||
}
|
||||
|
||||
/// Read vec3 (3 f32s) from shared memory
|
||||
#[inline]
|
||||
pub unsafe fn read_vec3(ptr: *const i32) -> [f32; 3] {
|
||||
let p = ptr as *const f32;
|
||||
[p.read(), p.add(1).read(), p.add(2).read()]
|
||||
}
|
||||
|
||||
/// Read slice from shared memory
|
||||
#[inline]
|
||||
pub unsafe fn read_i32_slice(ptr: *const i32, len: usize) -> &[i32] {
|
||||
std::slice::from_raw_parts(ptr, len)
|
||||
}
|
||||
|
||||
/// Read f32 slice from shared memory
|
||||
#[inline]
|
||||
pub unsafe fn read_f32_slice(ptr: *const i32, len: usize) -> &[f32] {
|
||||
std::slice::from_raw_parts(ptr as *const f32, len)
|
||||
}
|
||||
|
||||
/// Read with default value
|
||||
#[inline]
|
||||
pub unsafe fn read_f32_default(ptr: *const i32, default: f32) -> f32 {
|
||||
if ptr.is_null() { default } else { read_f32(ptr) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub unsafe fn read_i32_default(ptr: *const i32, default: i32) -> i32 {
|
||||
if ptr.is_null() { default } else { read_i32(ptr) }
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 4: Node Implementation Examples
|
||||
|
||||
### 4.1 Math Node
|
||||
|
||||
```rust
|
||||
// nodes/max/plantarium/math/src/lib.rs
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(op_type: *const i32, a: *const i32, b: *const i32) -> Vec<i32> {
|
||||
use nodarium_utils::{read_i32, read_f32};
|
||||
|
||||
let op = unsafe { read_i32(op_type) };
|
||||
let a_val = unsafe { read_f32(a) };
|
||||
let b_val = unsafe { read_f32(b) };
|
||||
|
||||
let result = match op {
|
||||
0 => a_val + b_val, // add
|
||||
1 => a_val - b_val, // subtract
|
||||
2 => a_val * b_val, // multiply
|
||||
3 => a_val / b_val, // divide
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
vec![result.to_bits()]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Vec3 Node
|
||||
|
||||
```rust
|
||||
// nodes/max/plantarium/vec3/src/lib.rs
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(x: *const i32, y: *const i32, z: *const i32) -> Vec<i32> {
|
||||
use nodarium_utils::read_f32;
|
||||
|
||||
let x_val = unsafe { read_f32(x) };
|
||||
let y_val = unsafe { read_f32(y) };
|
||||
let z_val = unsafe { read_f32(z) };
|
||||
|
||||
vec![x_val.to_bits(), y_val.to_bits(), z_val.to_bits()]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Box Node
|
||||
|
||||
```rust
|
||||
// nodes/max/plantarium/box/src/lib.rs
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(size: *const i32) -> Vec<i32> {
|
||||
use nodarium_utils::{read_f32, encode_float, calculate_normals};
|
||||
|
||||
let size = unsafe { read_f32(size) };
|
||||
let p = encode_float(size);
|
||||
let n = encode_float(-size);
|
||||
|
||||
let mut cube_geometry = vec![
|
||||
1, // 1: geometry
|
||||
8, // 8 vertices
|
||||
12, // 12 faces
|
||||
|
||||
// Face indices
|
||||
0, 1, 2, 0, 2, 3,
|
||||
0, 3, 4, 4, 5, 0,
|
||||
6, 1, 0, 5, 6, 0,
|
||||
7, 2, 1, 6, 7, 1,
|
||||
2, 7, 3, 3, 7, 4,
|
||||
7, 6, 4, 4, 6, 5,
|
||||
|
||||
// Bottom plate
|
||||
p, n, n, p, n, p, n, n, p, n, n, n,
|
||||
|
||||
// Top plate
|
||||
n, p, n, p, p, n, p, p, p, n, p, p,
|
||||
|
||||
// Normals
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
calculate_normals(&mut cube_geometry);
|
||||
cube_geometry
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Stem Node
|
||||
|
||||
```rust
|
||||
// nodes/max/plantarium/stem/src/lib.rs
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(
|
||||
origin: *const i32,
|
||||
amount: *const i32,
|
||||
length: *const i32,
|
||||
thickness: *const i32,
|
||||
resolution: *const i32,
|
||||
) -> Vec<i32> {
|
||||
use nodarium_utils::{
|
||||
read_vec3, read_i32, read_f32,
|
||||
geometry::{create_multiple_paths, wrap_multiple_paths},
|
||||
};
|
||||
|
||||
let origin = unsafe { read_vec3(origin) };
|
||||
let amount = unsafe { read_i32(amount) } as usize;
|
||||
let length = unsafe { read_f32(length) };
|
||||
let thickness = unsafe { read_f32(thickness) };
|
||||
let resolution = unsafe { read_i32(resolution) } as usize;
|
||||
|
||||
let mut stem_data = create_multiple_paths(amount, resolution, 1);
|
||||
let mut stems = wrap_multiple_paths(&mut stem_data);
|
||||
|
||||
for stem in stems.iter_mut() {
|
||||
let points = stem.get_points_mut();
|
||||
for (i, point) in points.iter_mut().enumerate() {
|
||||
let t = i as f32 / (resolution as f32 - 1.0);
|
||||
point.x = origin[0];
|
||||
point.y = origin[1] + t * length;
|
||||
point.z = origin[2];
|
||||
point.w = thickness * (1.0 - t);
|
||||
}
|
||||
}
|
||||
|
||||
stem_data
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 5: Runtime Implementation
|
||||
|
||||
```typescript
|
||||
// app/src/lib/runtime/memory-manager.ts
|
||||
|
||||
export const SHARED_MEMORY = new WebAssembly.Memory({
|
||||
initial: 1024, // 64MB initial
|
||||
maximum: 4096, // 256MB maximum
|
||||
});
|
||||
|
||||
export class MemoryManager {
|
||||
private offset: number = 0;
|
||||
private readonly start: number = 0;
|
||||
|
||||
reset() {
|
||||
this.offset = this.start;
|
||||
}
|
||||
|
||||
alloc(bytes: number): number {
|
||||
const pos = this.offset;
|
||||
this.offset += bytes;
|
||||
return pos;
|
||||
}
|
||||
|
||||
readInt32(pos: number): number {
|
||||
return new Int32Array(SHARED_MEMORY.buffer)[pos / 4];
|
||||
}
|
||||
|
||||
readFloat32(pos: number): number {
|
||||
return new Float32Array(SHARED_MEMORY.buffer)[pos / 4];
|
||||
}
|
||||
|
||||
readBytes(pos: number, length: number): Uint8Array {
|
||||
return new Uint8Array(SHARED_MEMORY.buffer, pos, length);
|
||||
}
|
||||
|
||||
getInt32View(): Int32Array {
|
||||
return new Int32Array(SHARED_MEMORY.buffer);
|
||||
}
|
||||
|
||||
getFloat32View(): Float32Array {
|
||||
return new Float32Array(SHARED_MEMORY.buffer);
|
||||
}
|
||||
|
||||
getRemaining(): number {
|
||||
return SHARED_MEMORY.buffer.byteLength - this.offset;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app/src/lib/runtime/imports.ts
|
||||
|
||||
import { SHARED_MEMORY } from "./memory-manager";
|
||||
|
||||
export function createImportObject(nodeId: string): WebAssembly.Imports {
|
||||
return {
|
||||
env: {
|
||||
// Import shared memory
|
||||
memory: SHARED_MEMORY,
|
||||
|
||||
// Logging
|
||||
__nodarium_log: (ptr: number, len: number) => {
|
||||
const msg = new TextDecoder().decode(
|
||||
new Uint8Array(SHARED_MEMORY.buffer, ptr, len),
|
||||
);
|
||||
console.log(`[${nodeId}] ${msg}`);
|
||||
},
|
||||
|
||||
__nodarium_log_panic: (ptr: number, len: number) => {
|
||||
const msg = new TextDecoder().decode(
|
||||
new Uint8Array(SHARED_MEMORY.buffer, ptr, len),
|
||||
);
|
||||
console.error(`[${nodeId}] PANIC: ${msg}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app/src/lib/runtime/executor.ts
|
||||
|
||||
import { SHARED_MEMORY } from "./memory-manager";
|
||||
import { createImportObject } from "./imports";
|
||||
|
||||
export class SharedMemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
private memory: MemoryManager;
|
||||
private results: Map<string, { pos: number; len: number }> = new Map();
|
||||
private instances: Map<string, WebAssembly.Instance> = new Map();
|
||||
|
||||
constructor(private registry: NodeRegistry) {
|
||||
this.memory = new MemoryManager();
|
||||
}
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
this.memory.reset();
|
||||
this.results.clear();
|
||||
|
||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
||||
const sortedNodes = nodes.sort((a, b) => b.depth - a.depth);
|
||||
|
||||
for (const node of sortedNodes) {
|
||||
await this.executeNode(node, settings);
|
||||
}
|
||||
|
||||
const result = this.results.get(outputNode.id);
|
||||
const view = this.memory.getInt32View();
|
||||
return view.subarray(result.pos / 4, result.pos / 4 + result.len / 4);
|
||||
}
|
||||
|
||||
private async executeNode(
|
||||
node: RuntimeNode,
|
||||
settings: Record<string, unknown>,
|
||||
) {
|
||||
const def = this.definitionMap.get(node.type)!;
|
||||
const inputs = def.inputs || {};
|
||||
const inputNames = Object.keys(inputs);
|
||||
|
||||
const outputSize = this.estimateOutputSize(def);
|
||||
const outputPos = this.memory.alloc(outputSize);
|
||||
const args: number[] = [outputPos];
|
||||
|
||||
for (const inputName of inputNames) {
|
||||
const inputDef = inputs[inputName];
|
||||
const inputNode = node.state.inputNodes[inputName];
|
||||
if (inputNode) {
|
||||
const parentResult = this.results.get(inputNode.id)!;
|
||||
args.push(parentResult.pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
const valuePos = this.memory.alloc(16);
|
||||
this.writeValue(
|
||||
valuePos,
|
||||
inputDef,
|
||||
node.props?.[inputName] ??
|
||||
settings[inputDef.setting ?? ""] ??
|
||||
inputDef.value,
|
||||
);
|
||||
args.push(valuePos);
|
||||
}
|
||||
|
||||
let instance = this.instances.get(node.type);
|
||||
if (!instance) {
|
||||
instance = await this.instantiateNode(node.type);
|
||||
this.instances.set(node.type, instance);
|
||||
}
|
||||
|
||||
const writtenLen = instance.exports.execute(...args);
|
||||
this.results.set(node.id, { pos: outputPos, len: writtenLen });
|
||||
}
|
||||
|
||||
private writeValue(pos: number, inputDef: NodeInput, value: unknown) {
|
||||
const view = this.memory.getFloat32View();
|
||||
const intView = this.memory.getInt32View();
|
||||
|
||||
switch (inputDef.type) {
|
||||
case "float":
|
||||
view[pos / 4] = value as number;
|
||||
break;
|
||||
case "integer":
|
||||
case "select":
|
||||
case "seed":
|
||||
intView[pos / 4] = value as number;
|
||||
break;
|
||||
case "boolean":
|
||||
intView[pos / 4] = value ? 1 : 0;
|
||||
break;
|
||||
case "vec3":
|
||||
const arr = value as number[];
|
||||
view[pos / 4] = arr[0];
|
||||
view[pos / 4 + 1] = arr[1];
|
||||
view[pos / 4 + 2] = arr[2];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private estimateOutputSize(def: NodeDefinition): number {
|
||||
const sizes: Record<string, number> = {
|
||||
float: 16,
|
||||
integer: 16,
|
||||
boolean: 16,
|
||||
vec3: 16,
|
||||
geometry: 8192,
|
||||
path: 4096,
|
||||
};
|
||||
return sizes[def.outputs?.[0] || "float"] || 64;
|
||||
}
|
||||
|
||||
private async instantiateNode(
|
||||
nodeType: string,
|
||||
): Promise<WebAssembly.Instance> {
|
||||
const wasmBytes = await this.fetchWasm(nodeType);
|
||||
const module = await WebAssembly.compile(wasmBytes);
|
||||
const importObject = createImportObject(nodeType);
|
||||
return WebAssembly.instantiate(module, importObject);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 7: Execution Flow Visualization
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Execution Timeline │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Step 1: Setup
|
||||
SHARED_MEMORY = new WebAssembly.Memory({ initial: 1024 })
|
||||
memory.offset = 0
|
||||
|
||||
Step 2: Execute Node A (math with 3 inputs)
|
||||
outputPos = memory.alloc(16) = 0
|
||||
args = [0, ptr_to_op_type, ptr_to_a, ptr_to_b]
|
||||
|
||||
Node A reads:
|
||||
*ptr_to_op_type → op
|
||||
*ptr_to_a → a
|
||||
*ptr_to_b → b
|
||||
|
||||
Node A returns: vec![result.to_bits()]
|
||||
|
||||
Macro writes result directly to SHARED_MEMORY[0..4]
|
||||
Returns: 4
|
||||
|
||||
results['A'] = { pos: 0, len: 4 }
|
||||
memory.offset = 4
|
||||
|
||||
Step 3: Execute Node B (stem with 5 inputs, input[0] from A)
|
||||
outputPos = memory.alloc(4096) = 4
|
||||
args = [4, results['A'].pos, ptr_to_amount, ptr_to_length, ...]
|
||||
|
||||
Node B reads:
|
||||
*results['A'].pos → value from Node A
|
||||
*ptr_to_amount → amount
|
||||
...
|
||||
|
||||
Node B returns: stem_data Vec<i32> (1000 elements = 4000 bytes)
|
||||
|
||||
Macro writes stem_data directly to SHARED_MEMORY[4..4004]
|
||||
Returns: 4000
|
||||
|
||||
results['B'] = { pos: 4, len: 4000 }
|
||||
memory.offset = 4004
|
||||
|
||||
Step 4: Execute Node C (output, 1 input from B)
|
||||
outputPos = memory.alloc(16) = 4004
|
||||
args = [4004, results['B'].pos, results['B'].len]
|
||||
|
||||
Node C reads:
|
||||
*results['B'].pos → stem geometry
|
||||
|
||||
Node C returns: vec![1] (identity)
|
||||
Macro writes to SHARED_MEMORY[4004..4008]
|
||||
|
||||
results['C'] = { pos: 4004, len: 4 }
|
||||
|
||||
Final: Return SHARED_MEMORY[4004..4008] as geometry result
|
||||
```
|
||||
|
||||
## Phase 6: Memory Growth Strategy
|
||||
|
||||
```typescript
|
||||
class MemoryManager {
|
||||
alloc(bytes: number): number {
|
||||
const required = this.offset + bytes;
|
||||
const currentBytes = SHARED_MEMORY.buffer.byteLength;
|
||||
|
||||
if (required > currentBytes) {
|
||||
const pagesNeeded = Math.ceil((required - currentBytes) / 65536);
|
||||
const success = SHARED_MEMORY.grow(pagesNeeded);
|
||||
|
||||
if (!success) {
|
||||
throw new Error(`Out of memory: need ${bytes} bytes`);
|
||||
}
|
||||
|
||||
this.int32View = new Int32Array(SHARED_MEMORY.buffer);
|
||||
this.float32View = new Float32Array(SHARED_MEMORY.buffer);
|
||||
}
|
||||
|
||||
const pos = this.offset;
|
||||
this.offset += bytes;
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 8: Migration Checklist
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- [ ] Add `--import-memory` to Rust flags in `Cargo.toml`
|
||||
- [ ] Ensure no nodes export memory
|
||||
|
||||
### Runtime
|
||||
|
||||
- [ ] Create `SHARED_MEMORY` instance
|
||||
- [ ] Implement `MemoryManager` with alloc/read/write
|
||||
- [ ] Create import object factory
|
||||
- [ ] Implement `SharedMemoryRuntimeExecutor`
|
||||
|
||||
### Macro
|
||||
|
||||
- [ ] Parse definition JSON
|
||||
- [ ] Validate function signature (N params, Vec<i32> return)
|
||||
- [ ] Generate wrapper that writes return value to `output_pos`
|
||||
- [ ] Add panic hook
|
||||
|
||||
### Utilities
|
||||
|
||||
- [ ] `read_i32(ptr: *const i32) -> i32`
|
||||
- [ ] `read_f32(ptr: *const i32) -> f32`
|
||||
- [ ] `read_bool(ptr: *const i32) -> bool`
|
||||
- [ ] `read_vec3(ptr: *const i32) -> [f32; 3]`
|
||||
- [ ] `read_i32_slice(ptr: *const i32, len: usize) -> &[i32]`
|
||||
|
||||
### Nodes
|
||||
|
||||
- [ ] `float`, `integer`, `boolean` nodes
|
||||
- [ ] `vec3` node
|
||||
- [ ] `math` node
|
||||
- [ ] `random` node
|
||||
- [ ] `box` node
|
||||
- [ ] `stem` node
|
||||
- [ ] `branch` node
|
||||
- [ ] `instance` node
|
||||
- [ ] `output` node
|
||||
|
||||
## Phase 9: Before vs After
|
||||
|
||||
### Before (per-node memory)
|
||||
|
||||
```rust
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
let args = split_args(input);
|
||||
let a = evaluate_float(args[0]);
|
||||
let b = evaluate_float(args[1]);
|
||||
vec![(a + b).to_bits()]
|
||||
}
|
||||
```
|
||||
|
||||
### After (shared memory)
|
||||
|
||||
```rust
|
||||
#[nodarium_execute]
|
||||
pub fn execute(a: *const i32, b: *const i32) -> Vec<i32> {
|
||||
use nodarium_utils::read_f32;
|
||||
let a_val = unsafe { read_f32(a) };
|
||||
let b_val = unsafe { read_f32(b) };
|
||||
vec![(a_val + b_val).to_bits()]
|
||||
}
|
||||
```
|
||||
|
||||
**Key differences:**
|
||||
|
||||
- Parameters are input pointers, not a slice
|
||||
- Use `read_f32` helper instead of `evaluate_float`
|
||||
- Macro writes result directly to shared memory
|
||||
- All nodes share the same memory import
|
||||
|
||||
## Phase 10: Benefits
|
||||
|
||||
| Aspect | Before | After |
|
||||
| ----------------- | -------------- | -------------------- |
|
||||
| Memory | N × ~1MB heaps | 1 × 64-256MB shared |
|
||||
| Cross-node access | Copy via JS | Direct read |
|
||||
| API | `&[i32]` slice | `*const i32` pointer |
|
||||
| Validation | Runtime | Compile-time |
|
||||
227
SUMMARY.md
Normal file
227
SUMMARY.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Nodarium - AI Coding Agent Summary
|
||||
|
||||
## Project Overview
|
||||
|
||||
Nodarium is a WebAssembly-based visual programming language used to build <https://nodes.max-richter.dev>, a procedural 3D plant modeling tool. The system allows users to create visual node graphs where each node is a compiled WebAssembly module.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
**Frontend (SvelteKit):**
|
||||
|
||||
- Framework: SvelteKit with Svelte 5
|
||||
- 3D Rendering: Three.js via Threlte
|
||||
- Styling: Tailwind CSS 4
|
||||
- Build Tool: Vite
|
||||
- State Management: Custom store-client package
|
||||
- WASM Integration: vite-plugin-wasm, comlink
|
||||
|
||||
**Backend/Core (Rust/WASM):**
|
||||
|
||||
- Language: Rust
|
||||
- Output: WebAssembly (wasm32-unknown-unknown target)
|
||||
- Build Tool: cargo
|
||||
- Procedural Macros: custom macros package
|
||||
|
||||
**Package Management:**
|
||||
|
||||
- Node packages: pnpm workspace (v10.28.1)
|
||||
- Rust packages: Cargo workspace
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
nodarium/
|
||||
├── app/ # SvelteKit web application
|
||||
│ ├── src/
|
||||
│ │ ├── lib/ # App-specific components and utilities
|
||||
│ │ ├── routes/ # SvelteKit routes (pages)
|
||||
│ │ ├── app.css # Global styles
|
||||
│ │ └── app.html # HTML template
|
||||
│ ├── static/
|
||||
│ │ └── nodes/ # Compiled WASM node files served statically
|
||||
│ ├── package.json # App dependencies
|
||||
│ ├── svelte.config.js # SvelteKit configuration
|
||||
│ ├── vite.config.ts # Vite configuration
|
||||
│ └── tsconfig.json # TypeScript configuration
|
||||
│
|
||||
├── packages/ # Shared workspace packages
|
||||
│ ├── ui/ # Svelte UI component library (published as @nodarium/ui)
|
||||
│ │ ├── src/ # UI components
|
||||
│ │ ├── static/ # Static assets for UI
|
||||
│ │ ├── dist/ # Built output
|
||||
│ │ └── package.json
|
||||
│ ├── registry/ # Node registry with IndexedDB persistence (@nodarium/registry)
|
||||
│ │ └── src/
|
||||
│ ├── types/ # Shared TypeScript types (@nodarium/types)
|
||||
│ │ └── src/
|
||||
│ ├── utils/ # Shared utilities (@nodarium/utils)
|
||||
│ │ └── src/
|
||||
│ └── macros/ # Rust procedural macros for node development
|
||||
│
|
||||
├── nodes/ # WebAssembly node packages (Rust)
|
||||
│ └── max/plantarium/ # Plantarium nodes namespace
|
||||
│ ├── box/ # Box geometry node
|
||||
│ ├── branch/ # Branch generation node
|
||||
│ ├── float/ # Float value node
|
||||
│ ├── gravity/ # Gravity simulation node
|
||||
│ ├── instance/ # Geometry instancing node
|
||||
│ ├── math/ # Math operations node
|
||||
│ ├── noise/ # Noise generation node
|
||||
│ ├── output/ # Output node for results
|
||||
│ ├── random/ # Random value node
|
||||
│ ├── rotate/ # Rotation transformation node
|
||||
│ ├── stem/ # Stem geometry node
|
||||
│ ├── triangle/ # Triangle geometry node
|
||||
│ ├── vec3/ # Vector3 manipulation node
|
||||
│ └── .template/ # Node template for creating new nodes
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
│ ├── ARCHITECTURE.md # System architecture overview
|
||||
│ ├── DEVELOPING_NODES.md # Guide for creating new nodes
|
||||
│ ├── NODE_DEFINITION.md # Node definition schema
|
||||
│ └── PLANTARIUM.md # Plantarium-specific documentation
|
||||
│
|
||||
├── Cargo.toml # Rust workspace configuration
|
||||
├── package.json # Root npm scripts
|
||||
├── pnpm-workspace.yaml # pnpm workspace configuration
|
||||
├── pnpm-lock.yaml # Locked dependency versions
|
||||
└── README.md # Project readme
|
||||
```
|
||||
|
||||
## Node System Architecture
|
||||
|
||||
### What is a Node?
|
||||
|
||||
Nodes are WebAssembly modules that:
|
||||
|
||||
- Have a unique ID (e.g., `max/plantarium/stem`)
|
||||
- Define inputs with types and default values
|
||||
- Define outputs they produce
|
||||
- Execute logic when called with arguments
|
||||
|
||||
### Node Definition Schema
|
||||
|
||||
Nodes are defined via `definition.json` embedded in each WASM module:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "namespace/category/node-name",
|
||||
"outputs": ["geometry"],
|
||||
"inputs": {
|
||||
"height": { "type": "float", "value": 1.0 },
|
||||
"radius": { "type": "float", "value": 0.1 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For now the outputs are limited to a single output.
|
||||
|
||||
### Node Execution
|
||||
|
||||
Nodes receive serialized arguments and return serialized outputs. The `nodarium_utils` Rust crate provides helpers for:
|
||||
|
||||
- Parsing input arguments
|
||||
- Creating geometry data
|
||||
- Concatenating output vectors
|
||||
|
||||
### Node Registration
|
||||
|
||||
Nodes are:
|
||||
|
||||
1. Compiled to WASM files in `target/wasm32-unknown-unknown/release/`
|
||||
2. Copied to `app/static/nodes/` for serving
|
||||
3. Registered in the browser via IndexedDB using the registry package
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
**Frontend:**
|
||||
|
||||
- `@sveltejs/kit` - Application framework
|
||||
- `@threlte/core` & `@threlte/extras` - Three.js Svelte integration
|
||||
- `three` - 3D graphics library
|
||||
- `tailwindcss` - CSS framework
|
||||
- `comlink` - WebWorker RPC
|
||||
- `idb` - IndexedDB wrapper
|
||||
- `wabt` - WebAssembly binary toolkit
|
||||
|
||||
**Rust/WASM:**
|
||||
|
||||
- Language: Rust (compiled with plain cargo)
|
||||
- Output: WebAssembly (wasm32-unknown-unknown target)
|
||||
- Generic WASM wrapper for language-agnostic node development
|
||||
- `glam` - Math library (Vec2, Vec3, Mat4, etc.)
|
||||
- `nodarium_macros` - Custom procedural macros
|
||||
- `nodarium_utils` - Shared node utilities
|
||||
|
||||
## Build Commands
|
||||
|
||||
From root directory:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm i
|
||||
|
||||
# Build all WASM nodes (compiles Rust, copies to app/static)
|
||||
pnpm build:nodes
|
||||
|
||||
# Build the app (builds UI library + SvelteKit app)
|
||||
pnpm build:app
|
||||
|
||||
# Full build (nodes + app)
|
||||
pnpm build
|
||||
|
||||
# Development
|
||||
pnpm dev # Run all dev commands in parallel
|
||||
pnpm dev:nodes # Watch nodes/, auto-rebuild on changes
|
||||
pnpm dev:app_ui # Watch app and UI package
|
||||
pnpm dev_ui # Watch UI package only
|
||||
```
|
||||
|
||||
## Workspace Packages
|
||||
|
||||
The project uses pnpm workspaces with the following packages:
|
||||
|
||||
| Package | Location | Purpose |
|
||||
| ------------------ | ------------------ | ------------------------------ |
|
||||
| @nodarium/app | app/ | Main SvelteKit application |
|
||||
| @nodarium/ui | packages/ui/ | Reusable UI component library |
|
||||
| @nodarium/registry | packages/registry/ | Node registry with persistence |
|
||||
| @nodarium/types | packages/types/ | Shared TypeScript types |
|
||||
| @nodarium/utils | packages/utils/ | Shared utilities |
|
||||
| nodarium macros | packages/macros/ | Rust procedural macros |
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- `.dprint.jsonc` - Dprint formatter configuration
|
||||
- `svelte.config.js` - SvelteKit configuration (app and ui)
|
||||
- `vite.config.ts` - Vite bundler configuration
|
||||
- `tsconfig.json` - TypeScript configuration (app and packages)
|
||||
- `Cargo.toml` - Rust workspace with member packages
|
||||
- `flake.nix` - Nix development environment
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Adding a New Node
|
||||
|
||||
1. Copy the `.template` directory in `nodes/max/plantarium/` to create a new node directory
|
||||
2. Define node in `src/definition.json`
|
||||
3. Implement logic in `src/lib.rs`
|
||||
4. Build with `cargo build --release --target wasm32-unknown-unknown`
|
||||
5. Test by dragging onto the node graph
|
||||
|
||||
### Modifying UI Components
|
||||
|
||||
1. Changes to `packages/ui/` automatically rebuild with watch mode
|
||||
2. App imports from `@nodarium/ui`
|
||||
3. Run `pnpm dev:app_ui` for hot reload
|
||||
|
||||
## Important Notes for AI Agents
|
||||
|
||||
1. **WASM Compilation**: Nodes require `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`)
|
||||
2. **Cross-Compilation**: WASM build happens on host, not in containers/VMs
|
||||
3. **Static Serving**: Compiled WASM files must exist in `app/static/nodes/` before dev server runs
|
||||
4. **Workspace Dependencies**: Use `workspace:*` protocol for internal packages
|
||||
5. **Threlte Version**: Uses Threlte 8.x, not 7.x (important for 3D component APIs)
|
||||
6. **Svelte 5**: Project uses Svelte 5 with runes (`$state`, `$derived`, `$effect`)
|
||||
7. **Tailwind 4**: Uses Tailwind CSS v4 with `@tailwindcss/vite` plugin
|
||||
8. **IndexedDB**: Registry uses IDB for persistent node storage in browser
|
||||
294
SUMMARY_RUNTIME.md
Normal file
294
SUMMARY_RUNTIME.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Node Compilation and Runtime Execution
|
||||
|
||||
## Overview
|
||||
|
||||
Nodarium nodes are WebAssembly modules written in Rust. Each node is a compiled WASM binary that exposes a standardized C ABI interface. The system uses procedural macros to generate the necessary boilerplate for node definitions, memory management, and execution.
|
||||
|
||||
## Node Compilation
|
||||
|
||||
### 1. Node Definition (JSON)
|
||||
|
||||
Each node has a `src/input.json` file that defines:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "max/plantarium/stem",
|
||||
"meta": { "description": "Creates a stem" },
|
||||
"outputs": ["path"],
|
||||
"inputs": {
|
||||
"origin": { "type": "vec3", "value": [0, 0, 0], "external": true },
|
||||
"amount": { "type": "integer", "value": 1, "min": 1, "max": 64 },
|
||||
"length": { "type": "float", "value": 5 },
|
||||
"thickness": { "type": "float", "value": 0.2 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Procedural Macros
|
||||
|
||||
The `nodarium_macros` crate provides two procedural macros:
|
||||
|
||||
#### `#[nodarium_execute]`
|
||||
|
||||
Transforms a Rust function into a WASM-compatible entry point:
|
||||
|
||||
```rust
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
// Node logic here
|
||||
}
|
||||
```
|
||||
|
||||
The macro generates:
|
||||
- **C ABI wrapper**: Converts the WASM interface to a standard C FFI
|
||||
- **`execute` function**: Takes `(ptr: *const i32, len: usize)` and returns `*mut i32`
|
||||
- **Memory allocation**: `__alloc(len: usize) -> *mut i32` for buffer allocation
|
||||
- **Memory deallocation**: `__free(ptr: *mut i32, len: usize)` for cleanup
|
||||
- **Static output buffer**: `OUTPUT_BUFFER` for returning results
|
||||
- **Panic hook**: Routes panics through `host_log_panic` for debugging
|
||||
- **Internal logic wrapper**: Wraps the original function
|
||||
|
||||
#### `nodarium_definition_file!("path")`
|
||||
|
||||
Embeds the node definition JSON into the WASM binary:
|
||||
|
||||
```rust
|
||||
nodarium_definition_file!("src/input.json");
|
||||
```
|
||||
|
||||
Generates:
|
||||
- **`DEFINITION_DATA`**: Static byte array in `nodarium_definition` section
|
||||
- **`get_definition_ptr()`**: Returns pointer to definition data
|
||||
- **`get_definition_len()`**: Returns length of definition data
|
||||
|
||||
### 3. Build Process
|
||||
|
||||
Nodes are compiled with:
|
||||
```bash
|
||||
cargo build --release --target wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
The resulting `.wasm` files are copied to `app/static/nodes/` for serving.
|
||||
|
||||
## Node Execution Runtime
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ WebWorker Thread │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ WorkerRuntimeExecutor ││
|
||||
│ │ ┌───────────────────────────────────────────────────┐ ││
|
||||
│ │ │ MemoryRuntimeExecutor ││
|
||||
│ │ │ ┌─────────────────────────────────────────────┐ ││
|
||||
│ │ │ │ Node Registry (WASM + Definitions) ││
|
||||
│ │ │ └─────────────────────────────────────────────┘ ││
|
||||
│ │ │ ┌─────────────────────────────────────────────┐ ││
|
||||
│ │ │ │ Execution Engine (Bottom-Up Evaluation) ││
|
||||
│ │ │ └─────────────────────────────────────────────┘ ││
|
||||
│ │ └───────────────────────────────────────────────────┘ ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1. MemoryRuntimeExecutor
|
||||
|
||||
The core execution engine in `runtime-executor.ts`:
|
||||
|
||||
#### Metadata Collection (`addMetaData`)
|
||||
|
||||
1. Load node definitions from registry
|
||||
2. Build parent/child relationships from graph edges
|
||||
3. Calculate execution depth via reverse BFS from output node
|
||||
|
||||
#### Node Sorting
|
||||
|
||||
Nodes are sorted by depth (highest depth first) for bottom-up execution:
|
||||
|
||||
```
|
||||
Depth 3: n3 n6
|
||||
Depth 2: n2 n4 n5
|
||||
Depth 1: n1
|
||||
Depth 0: Output
|
||||
Execution order: n3, n6, n2, n4, n5, n1, Output
|
||||
```
|
||||
|
||||
#### Input Collection
|
||||
|
||||
For each node, inputs are gathered from:
|
||||
1. **Connected nodes**: Results from parent nodes in the graph
|
||||
2. **Node props**: Values stored directly on the node instance
|
||||
3. **Settings**: Global settings mapped via `setting` property
|
||||
4. **Defaults**: Values from node definition
|
||||
|
||||
#### Input Encoding
|
||||
|
||||
Values are encoded as `Int32Array`:
|
||||
- **Floats**: IEEE 754 bits cast to i32
|
||||
- **Vectors**: `[0, count, v1, v2, v3, 1, 1]` (nested bracket format)
|
||||
- **Booleans**: `0` or `1`
|
||||
- **Integers**: Direct i32 value
|
||||
|
||||
#### Caching
|
||||
|
||||
Results are cached using:
|
||||
```typescript
|
||||
inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`
|
||||
```
|
||||
|
||||
The cache uses LRU eviction (default size: 50 entries).
|
||||
|
||||
### 2. Execution Flow
|
||||
|
||||
```typescript
|
||||
async execute(graph: Graph, settings) {
|
||||
// 1. Load definitions and build node relationships
|
||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
||||
|
||||
// 2. Sort nodes by depth (bottom-up)
|
||||
const sortedNodes = nodes.sort((a, b) => b.depth - a.depth);
|
||||
|
||||
// 3. Execute each node
|
||||
for (const node of sortedNodes) {
|
||||
const inputs = this.collectInputs(node, settings);
|
||||
const encoded = concatEncodedArrays(inputs);
|
||||
const result = nodeType.execute(encoded);
|
||||
this.results[node.id] = result;
|
||||
}
|
||||
|
||||
// 4. Return output node result
|
||||
return this.results[outputNode.id];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker Isolation
|
||||
|
||||
`WorkerRuntimeExecutor` runs execution in a WebWorker via Comlink:
|
||||
|
||||
```typescript
|
||||
class WorkerRuntimeExecutor implements RuntimeExecutor {
|
||||
private worker = new ComlinkWorker(...);
|
||||
|
||||
async execute(graph, settings) {
|
||||
return this.worker.executeGraph(graph, settings);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The worker backend (`worker-runtime-executor-backend.ts`):
|
||||
- Creates a single `MemoryRuntimeExecutor` instance
|
||||
- Manages caching state
|
||||
- Collects performance metrics
|
||||
|
||||
### 4. Remote Execution (Optional)
|
||||
|
||||
`RemoteRuntimeExecutor` can execute graphs on a remote server:
|
||||
|
||||
```typescript
|
||||
class RemoteRuntimeExecutor implements RuntimeExecutor {
|
||||
async execute(graph, settings) {
|
||||
const res = await fetch(this.url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ graph, settings })
|
||||
});
|
||||
return new Int32Array(await res.arrayBuffer());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Encoding Format
|
||||
|
||||
### Bracket Notation
|
||||
|
||||
Inputs and outputs use a nested bracket encoding:
|
||||
|
||||
```
|
||||
[0, count, item1, item2, ..., 1, 1]
|
||||
^ ^ items ^ ^
|
||||
| | | |
|
||||
| | | +-- closing bracket
|
||||
| +-- number of items + 1 |
|
||||
+-- opening bracket (0) +-- closing bracket (1)
|
||||
```
|
||||
|
||||
### Example Encodings
|
||||
|
||||
**Float (5.0)**:
|
||||
```typescript
|
||||
encodeFloat(5.0) // → 1084227584 (IEEE 754 bits as i32)
|
||||
```
|
||||
|
||||
**Vec3 ([1, 2, 3])**:
|
||||
```typescript
|
||||
[0, 4, encodeFloat(1), encodeFloat(2), encodeFloat(3), 1, 1]
|
||||
```
|
||||
|
||||
**Nested Math Expression**:
|
||||
```
|
||||
[0, 3, 0, 2, 0, 3, 0, 0, 0, 3, 7549747, 127, 1, 1, ...]
|
||||
```
|
||||
|
||||
### Decoding Utilities
|
||||
|
||||
From `packages/utils/src/tree.rs`:
|
||||
- `split_args()`: Parses nested bracket arrays into segments
|
||||
- `evaluate_float()`: Recursively evaluates and decodes float expressions
|
||||
- `evaluate_int()`: Evaluates integer/math node expressions
|
||||
- `evaluate_vec3()`: Decodes vec3 arrays
|
||||
|
||||
## Geometry Data Format
|
||||
|
||||
### Path Data
|
||||
|
||||
Paths represent procedural plant structures:
|
||||
|
||||
```
|
||||
[0, count, [0, header_size, node_type, depth, x, y, z, w, ...], 1, 1]
|
||||
```
|
||||
|
||||
Each point has 4 values: x, y, z position + thickness (w).
|
||||
|
||||
### Geometry Data
|
||||
|
||||
Meshes use a similar format with vertices and face indices.
|
||||
|
||||
## Performance Tracking
|
||||
|
||||
The runtime collects detailed performance metrics:
|
||||
- `collect-metadata`: Time to build node graph
|
||||
- `collected-inputs`: Time to gather inputs
|
||||
- `encoded-inputs`: Time to encode inputs
|
||||
- `hash-inputs`: Time to compute cache hash
|
||||
- `cache-hit`: 1 if cache hit, 0 if miss
|
||||
- `node/{node_type}`: Time per node execution
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
### MemoryRuntimeCache
|
||||
|
||||
LRU cache implementation:
|
||||
```typescript
|
||||
class MemoryRuntimeCache {
|
||||
private map = new Map<string, unknown>();
|
||||
size: number = 50;
|
||||
|
||||
get(key) { /* move to front */ }
|
||||
set(key, value) { /* evict oldest if at capacity */ }
|
||||
}
|
||||
```
|
||||
|
||||
### IndexDBCache
|
||||
|
||||
For persistence across sessions, the registry uses IndexedDB caching.
|
||||
|
||||
## Summary
|
||||
|
||||
The Nodarium node system works as follows:
|
||||
|
||||
1. **Compilation**: Rust functions are decorated with macros that generate C ABI WASM exports
|
||||
2. **Registration**: Node definitions are embedded in WASM and loaded at runtime
|
||||
3. **Graph Analysis**: Runtime builds node relationships and execution order
|
||||
4. **Bottom-Up Execution**: Nodes execute from leaves to output
|
||||
5. **Caching**: Results are cached per-node-inputs hash for performance
|
||||
6. **Isolation**: Execution runs in a WebWorker to prevent main thread blocking
|
||||
@@ -28,5 +28,6 @@ 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
|
||||
COPY --from=builder /app/packages/ui/build /app/ui
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
1
app/benchmark/.gitignore
vendored
Normal file
1
app/benchmark/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
out/
|
||||
47
app/benchmark/benchmarkRegistry.ts
Normal file
47
app/benchmark/benchmarkRegistry.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
56
app/benchmark/index.ts
Normal file
56
app/benchmark/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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();
|
||||
95
app/benchmark/templates/default.json
Normal file
95
app/benchmark/templates/default.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
|
||||
"nodes": [
|
||||
{ "id": 9, "position": [220, 80], "type": "max/plantarium/output", "props": {} },
|
||||
{
|
||||
"id": 10,
|
||||
"position": [95, 80],
|
||||
"type": "max/plantarium/stem",
|
||||
"props": { "amount": 5, "length": 11, "thickness": 0.1 }
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"position": [195, 80],
|
||||
"type": "max/plantarium/gravity",
|
||||
"props": {
|
||||
"strength": 0.38,
|
||||
"scale": 39,
|
||||
"fixBottom": 0,
|
||||
"directionalStrength": [1, 1, 1],
|
||||
"depth": 1,
|
||||
"curviness": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"position": [120, 80],
|
||||
"type": "max/plantarium/noise",
|
||||
"props": {
|
||||
"strength": 4.9,
|
||||
"scale": 2.2,
|
||||
"fixBottom": 1,
|
||||
"directionalStrength": [1, 1, 1],
|
||||
"depth": 1,
|
||||
"octaves": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"position": [70, 80],
|
||||
"type": "max/plantarium/vec3",
|
||||
"props": { "0": 0, "1": 0, "2": 0 }
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"position": [45, 80],
|
||||
"type": "max/plantarium/random",
|
||||
"props": { "min": -2, "max": 2 }
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"position": [170, 80],
|
||||
"type": "max/plantarium/branch",
|
||||
"props": {
|
||||
"length": 1.6,
|
||||
"thickness": 0.69,
|
||||
"amount": 36,
|
||||
"offsetSingle": 0.5,
|
||||
"lowestBranch": 0.46,
|
||||
"highestBranch": 1,
|
||||
"depth": 1,
|
||||
"rotation": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"position": [145, 80],
|
||||
"type": "max/plantarium/gravity",
|
||||
"props": {
|
||||
"strength": 0.38,
|
||||
"scale": 39,
|
||||
"fixBottom": 0,
|
||||
"directionalStrength": [1, 1, 1],
|
||||
"depth": 1,
|
||||
"curviness": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"position": [70, 120],
|
||||
"type": "max/plantarium/random",
|
||||
"props": { "min": 0.073, "max": 0.15 }
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
[14, 0, 9, "input"],
|
||||
[10, 0, 15, "plant"],
|
||||
[16, 0, 10, "origin"],
|
||||
[17, 0, 16, "0"],
|
||||
[17, 0, 16, "2"],
|
||||
[18, 0, 14, "plant"],
|
||||
[15, 0, 19, "plant"],
|
||||
[19, 0, 18, "plant"],
|
||||
[20, 0, 10, "thickness"]
|
||||
]
|
||||
}
|
||||
44
app/benchmark/templates/lotta-faces.json
Normal file
44
app/benchmark/templates/lotta-faces.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"settings": { "resolution.circle": 64, "resolution.curve": 64, "randomSeed": false },
|
||||
"nodes": [
|
||||
{ "id": 9, "position": [260, 0], "type": "max/plantarium/output", "props": {} },
|
||||
{
|
||||
"id": 18,
|
||||
"position": [185, 0],
|
||||
"type": "max/plantarium/stem",
|
||||
"props": { "amount": 64, "length": 12, "thickness": 0.15 }
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"position": [210, 0],
|
||||
"type": "max/plantarium/noise",
|
||||
"props": { "scale": 1.3, "strength": 5.4 }
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"position": [235, 0],
|
||||
"type": "max/plantarium/branch",
|
||||
"props": { "length": 0.8, "thickness": 0.8, "amount": 3 }
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"position": [160, 0],
|
||||
"type": "max/plantarium/vec3",
|
||||
"props": { "0": 0.39, "1": 0, "2": 0.41 }
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"position": [130, 0],
|
||||
"type": "max/plantarium/random",
|
||||
"props": { "min": -2, "max": 2 }
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
[18, 0, 19, "plant"],
|
||||
[19, 0, 20, "plant"],
|
||||
[20, 0, 9, "input"],
|
||||
[21, 0, 18, "origin"],
|
||||
[22, 0, 21, "0"],
|
||||
[22, 0, 21, "2"]
|
||||
]
|
||||
}
|
||||
71
app/benchmark/templates/plant.json
Normal file
71
app/benchmark/templates/plant.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"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"]
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nodarium/app",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -14,7 +14,8 @@
|
||||
"format": "dprint fmt -c '../.dprint.jsonc' .",
|
||||
"format:check": "dprint check -c '../.dprint.jsonc' .",
|
||||
"lint": "eslint .",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"bench": "tsx ./benchmark/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodarium/ui": "workspace:*",
|
||||
@@ -51,6 +52,7 @@
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "^7.3.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
<title>Nodes</title>
|
||||
|
||||
@@ -5,19 +5,33 @@
|
||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||
|
||||
type Props = {
|
||||
paddingLeft?: number;
|
||||
paddingRight?: number;
|
||||
paddingTop?: number;
|
||||
paddingBottom?: number;
|
||||
onnode: (n: NodeInstance) => void;
|
||||
};
|
||||
|
||||
const { onnode }: Props = $props();
|
||||
const padding = 10;
|
||||
|
||||
const {
|
||||
paddingLeft = padding,
|
||||
paddingRight = padding,
|
||||
paddingTop = padding,
|
||||
paddingBottom = padding,
|
||||
onnode
|
||||
}: Props = $props();
|
||||
|
||||
const graph = getGraphManager();
|
||||
const graphState = getGraphState();
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let wrapper: HTMLDivElement;
|
||||
let value = $state<string>();
|
||||
let activeNodeId = $state<NodeId>();
|
||||
|
||||
const MENU_WIDTH = 150;
|
||||
const MENU_HEIGHT = 350;
|
||||
|
||||
const allNodes = graphState.activeSocket
|
||||
? graph.getPossibleNodes(graphState.activeSocket)
|
||||
: graph.getNodeDefinitions();
|
||||
@@ -79,19 +93,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
input.disabled = false;
|
||||
setTimeout(() => input.focus(), 50);
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const deltaY = rect.bottom - window.innerHeight;
|
||||
const deltaX = rect.right - window.innerWidth;
|
||||
if (deltaY > 0) {
|
||||
wrapper.style.marginTop = `-${deltaY + 30}px`;
|
||||
}
|
||||
if (deltaX > 0) {
|
||||
wrapper.style.marginLeft = `-${deltaX + 30}px`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -100,7 +147,7 @@
|
||||
position.z={graphState.addMenuPosition?.[1]}
|
||||
transform={false}
|
||||
>
|
||||
<div class="add-menu-wrapper" bind:this={wrapper}>
|
||||
<div class="add-menu-wrapper">
|
||||
<div class="header">
|
||||
<input
|
||||
id="add-menu"
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
toneMapped: false
|
||||
});
|
||||
|
||||
let lineColor = $state(colors.outline.clone().convertSRGBToLinear());
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
if (appSettings.value.theme === undefined) {
|
||||
return;
|
||||
}
|
||||
circleMaterial.color = colors.outline.clone().convertSRGBToLinear();
|
||||
lineColor = colors.outline.clone().convertSRGBToLinear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +32,7 @@
|
||||
import { CubicBezierCurve } from 'three/src/extras/curves/CubicBezierCurve.js';
|
||||
import { Vector2 } from 'three/src/math/Vector2.js';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
import MeshGradientLineMaterial from './MeshGradientLine/MeshGradientLineMaterial.svelte';
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
@@ -45,12 +43,17 @@
|
||||
y2: number;
|
||||
z: number;
|
||||
id?: string;
|
||||
inputType?: string;
|
||||
outputType?: string;
|
||||
};
|
||||
|
||||
const { x1, y1, x2, y2, z, id }: Props = $props();
|
||||
const { x1, y1, x2, y2, z, inputType = 'unknown', outputType = 'unknown', id }: Props = $props();
|
||||
|
||||
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 lastId: string | null = null;
|
||||
@@ -106,9 +109,9 @@
|
||||
position.z={y1}
|
||||
position.y={0.8}
|
||||
rotation.x={-Math.PI / 2}
|
||||
material={circleMaterial}
|
||||
>
|
||||
<T.CircleGeometry args={[0.5, 16]} />
|
||||
<T.MeshBasicMaterial color={inputColor} toneMapped={false} />
|
||||
</T.Mesh>
|
||||
|
||||
<T.Mesh
|
||||
@@ -119,6 +122,7 @@
|
||||
material={circleMaterial}
|
||||
>
|
||||
<T.CircleGeometry args={[0.5, 16]} />
|
||||
<T.MeshBasicMaterial color={outputColor} toneMapped={false} />
|
||||
</T.Mesh>
|
||||
|
||||
{#if graphState.hoveredEdgeId === id}
|
||||
@@ -126,7 +130,8 @@
|
||||
<MeshLineGeometry {points} />
|
||||
<MeshLineMaterial
|
||||
width={thickness * 5}
|
||||
color={lineColor}
|
||||
color={inputColor}
|
||||
tonemapped={false}
|
||||
opacity={0.5}
|
||||
transparent
|
||||
/>
|
||||
@@ -135,5 +140,10 @@
|
||||
|
||||
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
||||
<MeshLineGeometry {points} />
|
||||
<MeshLineMaterial width={thickness} color={lineColor} />
|
||||
<MeshGradientLineMaterial
|
||||
width={thickness}
|
||||
colorStart={inputColor}
|
||||
colorEnd={outputColor}
|
||||
tonemapped={false}
|
||||
/>
|
||||
</T.Mesh>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<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>
|
||||
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
}
|
||||
68
app/src/lib/graph-interface/edges/MeshGradientLine/types.ts
Normal file
68
app/src/lib/graph-interface/edges/MeshGradientLine/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -25,14 +25,15 @@ const clone = 'structuredClone' in self
|
||||
? self.structuredClone
|
||||
: (args: unknown) => JSON.parse(JSON.stringify(args));
|
||||
|
||||
function areSocketsCompatible(
|
||||
export function areSocketsCompatible(
|
||||
output: string | undefined,
|
||||
inputs: string | (string | undefined)[] | undefined
|
||||
) {
|
||||
if (output === '*') return true;
|
||||
if (Array.isArray(inputs) && output) {
|
||||
return inputs.includes(output);
|
||||
return inputs.includes('*') || inputs.includes(output);
|
||||
}
|
||||
return inputs === output;
|
||||
return inputs === output || inputs === '*';
|
||||
}
|
||||
|
||||
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
||||
@@ -268,14 +269,7 @@ export class GraphManager extends EventEmitter<{
|
||||
private _init(graph: Graph) {
|
||||
const nodes = new SvelteMap(
|
||||
graph.nodes.map((node) => {
|
||||
const nodeType = this.registry.getNode(node.type);
|
||||
const n = node as NodeInstance;
|
||||
if (nodeType) {
|
||||
n.state = {
|
||||
type: nodeType
|
||||
};
|
||||
}
|
||||
return [node.id, n];
|
||||
return [node.id, node as NodeInstance];
|
||||
})
|
||||
);
|
||||
|
||||
@@ -300,6 +294,30 @@ export class GraphManager extends EventEmitter<{
|
||||
this.execute();
|
||||
}
|
||||
|
||||
private async loadAllCollections() {
|
||||
// Fetch all nodes from all collections of the loaded nodes
|
||||
const nodeIds = Array.from(new Set([...this.graph.nodes.map((n) => n.type)]));
|
||||
const allCollections = new Set<`${string}/${string}`>();
|
||||
for (const id of nodeIds) {
|
||||
const [user, collection] = id.split('/');
|
||||
allCollections.add(`${user}/${collection}`);
|
||||
}
|
||||
|
||||
const allCollectionIds = await Promise
|
||||
.all([...allCollections]
|
||||
.map(async (collection) =>
|
||||
remoteRegistry
|
||||
.fetchCollection(collection)
|
||||
.then((collection: { nodes: { id: NodeId }[] }) => {
|
||||
return collection.nodes.map(n => n.id.replace(/\.wasm$/, '') as NodeId);
|
||||
})
|
||||
));
|
||||
|
||||
const missingNodeIds = [...new Set(allCollectionIds.flat())];
|
||||
|
||||
this.registry.load(missingNodeIds);
|
||||
}
|
||||
|
||||
async load(graph: Graph) {
|
||||
const a = performance.now();
|
||||
|
||||
@@ -384,7 +402,9 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
this.loaded = true;
|
||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||
|
||||
setTimeout(() => this.execute(), 100);
|
||||
this.loadAllCollections(); // lazily load all nodes from all collections
|
||||
}
|
||||
|
||||
getAllNodes() {
|
||||
@@ -491,10 +511,10 @@ export class GraphManager extends EventEmitter<{
|
||||
const inputs = Object.entries(to.state?.type?.inputs ?? {});
|
||||
const outputs = from.state?.type?.outputs ?? [];
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const [inputName, input] = inputs[0];
|
||||
const [inputName, input] = inputs[i];
|
||||
for (let o = 0; o < outputs.length; o++) {
|
||||
const output = outputs[0];
|
||||
if (input.type === output) {
|
||||
const output = outputs[o];
|
||||
if (input.type === output || input.type === '*') {
|
||||
return this.createEdge(from, o, to, inputName);
|
||||
}
|
||||
}
|
||||
@@ -596,11 +616,14 @@ export class GraphManager extends EventEmitter<{
|
||||
return;
|
||||
}
|
||||
|
||||
const fromType = from.state.type || this.registry.getNode(from.type);
|
||||
const toType = to.state.type || this.registry.getNode(to.type);
|
||||
|
||||
// check if socket types match
|
||||
const fromSocketType = from.state?.type?.outputs?.[fromSocket];
|
||||
const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
|
||||
if (to.state?.type?.inputs?.[toSocket]?.accepts) {
|
||||
toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
|
||||
const fromSocketType = fromType?.outputs?.[fromSocket];
|
||||
const toSocketType = [toType?.inputs?.[toSocket]?.type];
|
||||
if (toType?.inputs?.[toSocket]?.accepts) {
|
||||
toSocketType.push(...(toType?.inputs?.[toSocket]?.accepts || []));
|
||||
}
|
||||
|
||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||
@@ -723,8 +746,9 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
||||
const nodeType = node?.state?.type;
|
||||
const nodeType = this.registry.getNode(node.type);
|
||||
if (!nodeType) return [];
|
||||
console.log({ index });
|
||||
|
||||
const sockets: [NodeInstance, string | number][] = [];
|
||||
|
||||
@@ -739,7 +763,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const ownType = nodeType?.inputs?.[index].type;
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeType = node?.state?.type;
|
||||
const nodeType = this.registry.getNode(node.type);
|
||||
const inputs = nodeType?.outputs;
|
||||
if (!inputs) continue;
|
||||
for (let index = 0; index < inputs.length; index++) {
|
||||
@@ -771,7 +795,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const ownType = nodeType.outputs?.[index];
|
||||
|
||||
for (const node of nodes) {
|
||||
const inputs = node?.state?.type?.inputs;
|
||||
const inputs = this.registry.getNode(node.type)?.inputs;
|
||||
if (!inputs) continue;
|
||||
for (const key in inputs) {
|
||||
const otherType = [inputs[key].type];
|
||||
@@ -787,6 +811,7 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${sockets.length} possible sockets`, sockets);
|
||||
return sockets;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getContext, setContext } from 'svelte';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import type { OrthographicCamera, Vector3 } from 'three';
|
||||
import type { GraphManager } from './graph-manager.svelte';
|
||||
import { ColorGenerator } from './graph/colors';
|
||||
import { getNodeHeight, getSocketPosition } from './helpers/nodeHelpers';
|
||||
|
||||
const graphStateKey = Symbol('graph-state');
|
||||
export function getGraphState() {
|
||||
@@ -27,7 +29,32 @@ type EdgeData = {
|
||||
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 {
|
||||
colors = new ColorGenerator(predefinedColors);
|
||||
|
||||
constructor(private graph: GraphManager) {
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
@@ -159,54 +186,27 @@ export class GraphState {
|
||||
return 1;
|
||||
}
|
||||
|
||||
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 {
|
||||
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
||||
return [
|
||||
node?.state?.x ?? node.position[0],
|
||||
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private nodeHeightCache: Record<string, number> = {};
|
||||
getNodeHeight(nodeTypeId: string) {
|
||||
if (nodeTypeId in this.nodeHeightCache) {
|
||||
return this.nodeHeightCache[nodeTypeId];
|
||||
}
|
||||
const node = this.graph.getNodeType(nodeTypeId);
|
||||
if (!node?.inputs) {
|
||||
return 5;
|
||||
}
|
||||
let height = 5;
|
||||
|
||||
for (const key of Object.keys(node.inputs)) {
|
||||
if (key === 'seed') continue;
|
||||
if (!node.inputs) continue;
|
||||
if (node?.inputs?.[key] === undefined) continue;
|
||||
if ('setting' in node.inputs[key]) continue;
|
||||
if (node.inputs[key].hidden) continue;
|
||||
if (
|
||||
node.inputs[key].type === 'shape'
|
||||
&& node.inputs[key].external !== true
|
||||
&& node.inputs[key].internal !== false
|
||||
) {
|
||||
height += 20;
|
||||
continue;
|
||||
tryConnectToDebugNode(nodeId: number) {
|
||||
const node = this.graph.nodes.get(nodeId);
|
||||
if (!node) return;
|
||||
if (node.type.endsWith('/debug')) return;
|
||||
if (!node.state.type?.outputs?.length) return;
|
||||
for (const _node of this.graph.nodes.values()) {
|
||||
if (_node.type.endsWith('/debug')) {
|
||||
this.graph.createEdge(node, 0, _node, 'input');
|
||||
return;
|
||||
}
|
||||
height += 10;
|
||||
}
|
||||
|
||||
this.nodeHeightCache[nodeTypeId] = height;
|
||||
return height;
|
||||
const debugNode = this.graph.createNode({
|
||||
type: 'max/plantarium/debug',
|
||||
position: [node.position[0] + 30, node.position[1]],
|
||||
props: {}
|
||||
});
|
||||
|
||||
if (debugNode) {
|
||||
this.graph.createEdge(node, 0, debugNode, 'input');
|
||||
}
|
||||
}
|
||||
|
||||
copyNodes() {
|
||||
@@ -259,14 +259,14 @@ export class GraphState {
|
||||
|
||||
let { node, index, position } = socket;
|
||||
|
||||
// remove existing edge
|
||||
// if the socket is an input socket -> remove existing edges
|
||||
if (typeof index === 'string') {
|
||||
const edges = this.graph.getEdgesToNode(node);
|
||||
for (const edge of edges) {
|
||||
if (edge[3] === index) {
|
||||
node = edge[0];
|
||||
index = edge[1];
|
||||
position = this.getSocketPosition(node, index);
|
||||
position = getSocketPosition(node, index);
|
||||
this.graph.removeEdge(edge);
|
||||
break;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ export class GraphState {
|
||||
return {
|
||||
node,
|
||||
index,
|
||||
position: this.getSocketPosition(node, index)
|
||||
position: getSocketPosition(node, index)
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -323,7 +323,7 @@ export class GraphState {
|
||||
for (const node of this.graph.nodes.values()) {
|
||||
const x = node.position[0];
|
||||
const y = node.position[1];
|
||||
const height = this.getNodeHeight(node.type);
|
||||
const height = getNodeHeight(node.state.type!);
|
||||
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
|
||||
clickedNodeId = node.id;
|
||||
break;
|
||||
@@ -335,14 +335,12 @@ export class GraphState {
|
||||
}
|
||||
|
||||
isNodeInView(node: NodeInstance) {
|
||||
const height = this.getNodeHeight(node.type);
|
||||
const height = getNodeHeight(node.state.type!);
|
||||
const width = 20;
|
||||
return (
|
||||
node.position[0] > this.cameraBounds[0] - width
|
||||
return node.position[0] > this.cameraBounds[0] - width
|
||||
&& node.position[0] < this.cameraBounds[1]
|
||||
&& node.position[1] > this.cameraBounds[2] - height
|
||||
&& node.position[1] < this.cameraBounds[3]
|
||||
);
|
||||
&& node.position[1] < this.cameraBounds[3];
|
||||
}
|
||||
|
||||
openNodePalette() {
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
import Debug from '../debug/Debug.svelte';
|
||||
import EdgeEl from '../edges/Edge.svelte';
|
||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||
import { getSocketPosition } from '../helpers/nodeHelpers';
|
||||
import NodeEl from '../node/Node.svelte';
|
||||
import { maxZoom, minZoom } from './constants';
|
||||
import { FileDropEventManager } from './drop.events';
|
||||
import { MouseEventManager } from './mouse.events';
|
||||
|
||||
const {
|
||||
keymap
|
||||
keymap,
|
||||
addMenuPadding
|
||||
}: {
|
||||
keymap: ReturnType<typeof createKeyMap>;
|
||||
addMenuPadding?: { left?: number; right?: number; bottom?: number; top?: number };
|
||||
} = $props();
|
||||
|
||||
const graph = getGraphManager();
|
||||
@@ -36,8 +39,8 @@
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
const pos1 = graphState.getSocketPosition(fromNode, edge[1]);
|
||||
const pos2 = graphState.getSocketPosition(toNode, edge[3]);
|
||||
const pos1 = getSocketPosition(fromNode, edge[1]);
|
||||
const pos2 = getSocketPosition(toNode, edge[3]);
|
||||
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
||||
}
|
||||
|
||||
@@ -92,6 +95,13 @@
|
||||
graphState.activeSocket = 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>
|
||||
|
||||
<svelte:window
|
||||
@@ -160,12 +170,20 @@
|
||||
|
||||
{#if graph.status === 'idle'}
|
||||
{#if graphState.addMenuPosition}
|
||||
<AddMenu onnode={handleNodeCreation} />
|
||||
<AddMenu
|
||||
onnode={handleNodeCreation}
|
||||
paddingTop={addMenuPadding?.top}
|
||||
paddingRight={addMenuPadding?.right}
|
||||
paddingBottom={addMenuPadding?.bottom}
|
||||
paddingLeft={addMenuPadding?.left}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if graphState.activeSocket}
|
||||
<EdgeEl
|
||||
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]}
|
||||
y1={graphState.activeSocket.position[1]}
|
||||
x2={graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0]}
|
||||
@@ -178,6 +196,8 @@
|
||||
<EdgeEl
|
||||
id={graph.getEdgeId(edge)}
|
||||
z={graphState.cameraPosition[2]}
|
||||
inputType={getSocketType(edge[0], edge[1])}
|
||||
outputType={getSocketType(edge[2], edge[3])}
|
||||
{x1}
|
||||
{y1}
|
||||
{x2}
|
||||
@@ -200,7 +220,6 @@
|
||||
<NodeEl
|
||||
{node}
|
||||
inView={graphState.isNodeInView(node)}
|
||||
z={graphState.cameraPosition[2]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
showHelp?: boolean;
|
||||
settingTypes?: Record<string, unknown>;
|
||||
|
||||
addMenuPadding?: { left?: number; right?: number; bottom?: number; top?: number };
|
||||
|
||||
onsave?: (save: Graph) => void;
|
||||
onresult?: (result: unknown) => void;
|
||||
};
|
||||
@@ -25,11 +27,13 @@
|
||||
let {
|
||||
graph,
|
||||
registry,
|
||||
addMenuPadding,
|
||||
settings = $bindable(),
|
||||
activeNode = $bindable(),
|
||||
backgroundType = $bindable('grid'),
|
||||
snapToGrid = $bindable(true),
|
||||
showHelp = $bindable(false),
|
||||
settings = $bindable(),
|
||||
settingTypes = $bindable(),
|
||||
onsave,
|
||||
onresult
|
||||
@@ -83,4 +87,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<GraphEl {keymap} />
|
||||
<GraphEl {keymap} {addMenuPadding} />
|
||||
|
||||
44
app/src/lib/graph-interface/graph/colors.ts
Normal file
44
app/src/lib/graph-interface/graph/colors.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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}%)`;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { type NodeInstance } from '@nodarium/types';
|
||||
import type { GraphManager } from '../graph-manager.svelte';
|
||||
import { type GraphState } from '../graph-state.svelte';
|
||||
import { snapToGrid as snapPointToGrid } from '../helpers';
|
||||
import { getNodeHeight } from '../helpers/nodeHelpers';
|
||||
import { maxZoom, minZoom, zoomSpeed } from './constants';
|
||||
import { EdgeInteractionManager } from './edge.events';
|
||||
|
||||
@@ -188,6 +189,10 @@ export class MouseEventManager {
|
||||
|
||||
// if we clicked on a node
|
||||
if (clickedNodeId !== -1) {
|
||||
if (event.ctrlKey && event.shiftKey) {
|
||||
this.state.tryConnectToDebugNode(clickedNodeId);
|
||||
return;
|
||||
}
|
||||
if (this.state.activeNodeId === -1) {
|
||||
this.state.activeNodeId = clickedNodeId;
|
||||
// if the selected node is the same as the clicked node
|
||||
@@ -289,7 +294,7 @@ export class MouseEventManager {
|
||||
if (!node?.state) continue;
|
||||
const x = node.position[0];
|
||||
const y = node.position[1];
|
||||
const height = this.state.getNodeHeight(node.type);
|
||||
const height = getNodeHeight(node.state.type!);
|
||||
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
||||
this.state.selectedNodes?.add(node.id);
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,9 @@ export function createNodePath({
|
||||
rightBump = false,
|
||||
aspectRatio = 1
|
||||
} = {}) {
|
||||
const leftBumpTopY = y + height / 2;
|
||||
const leftBumpBottomY = y - height / 2;
|
||||
|
||||
return `M0,${cornerTop}
|
||||
${
|
||||
cornerTop
|
||||
@@ -64,9 +67,7 @@ export function createNodePath({
|
||||
}
|
||||
${
|
||||
leftBump
|
||||
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
|
||||
y - height / 2
|
||||
}`
|
||||
? ` V${leftBumpTopY} C${depth},${leftBumpTopY} ${depth},${leftBumpBottomY} 0,${leftBumpBottomY}`
|
||||
: ` H0`
|
||||
}
|
||||
Z`.replace(/\s+/g, ' ');
|
||||
|
||||
71
app/src/lib/graph-interface/helpers/nodeHelpers.ts
Normal file
71
app/src/lib/graph-interface/helpers/nodeHelpers.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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,56 +1,88 @@
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
uniform float uWidth;
|
||||
uniform float uHeight;
|
||||
uniform float uZoom;
|
||||
|
||||
uniform vec3 uColorDark;
|
||||
uniform vec3 uColorBright;
|
||||
|
||||
uniform vec3 uStrokeColor;
|
||||
uniform float uStrokeWidth;
|
||||
|
||||
const float uHeaderHeight = 5.0;
|
||||
uniform float uSectionHeights[16];
|
||||
uniform int uNumSections;
|
||||
|
||||
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) {
|
||||
vec2 q = abs(p) - b + 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 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 k4 = msign(p.x * p.y);
|
||||
float k5 = r * k2 + max(-q.x, 0.0);
|
||||
|
||||
float ra = s * round(k1 / s);
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
vec2 size = vec2(uWidth, uHeight);
|
||||
vec2 uv = (vUv - 0.5) * 2.0;
|
||||
vec2 uvCenter = (vUv - 0.5) * 2.0;
|
||||
|
||||
float u_border_radius = 0.4;
|
||||
vec4 distance = roundedBoxSDF(uv * size, size, u_border_radius*2.0, 0.0);
|
||||
vec4 boxData = roundedBoxSDF(uvCenter * size, size, borderRadius * 2.0, 0.0);
|
||||
float sceneSDF = boxData.w;
|
||||
|
||||
if (distance.w > 0.0 ) {
|
||||
// outside
|
||||
gl_FragColor = vec4(0.0,0.0,0.0, 0.0);
|
||||
}else{
|
||||
if (distance.w > -uStrokeWidth || mod(y+5.0, 10.0) < uStrokeWidth/2.0) {
|
||||
// draw the outer stroke
|
||||
gl_FragColor = vec4(uStrokeColor, 1.0);
|
||||
}else if (y<5.0){
|
||||
// draw the header
|
||||
gl_FragColor = vec4(uColorBright, 1.0);
|
||||
}else{
|
||||
gl_FragColor = vec4(uColorDark, 1.0);
|
||||
}
|
||||
vec2 headerDentPos = vec2(uWidth, uHeaderHeight * 0.5);
|
||||
float headerDentDist = sdCircle(vec2(x, y) - headerDentPos, dentRadius);
|
||||
sceneSDF = max(sceneSDF, -headerDentDist*2.0);
|
||||
|
||||
float currentYBoundary = uHeaderHeight;
|
||||
float previousYBoundary = uHeaderHeight;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (i >= uNumSections) break;
|
||||
|
||||
float sectionHeight = uSectionHeights[i];
|
||||
currentYBoundary += sectionHeight;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { type Mesh } from 'three';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
import { colors } from '../graph/colors.svelte';
|
||||
import { getNodeHeight, getParameterHeight } from '../helpers/nodeHelpers';
|
||||
import NodeFrag from './Node.frag';
|
||||
import NodeVert from './Node.vert';
|
||||
import NodeHtml from './NodeHTML.svelte';
|
||||
@@ -14,9 +15,10 @@
|
||||
type Props = {
|
||||
node: NodeInstance;
|
||||
inView: boolean;
|
||||
z: number;
|
||||
};
|
||||
let { node = $bindable(), inView, z }: Props = $props();
|
||||
let { node = $bindable(), inView }: Props = $props();
|
||||
|
||||
const nodeType = $derived(node.state.type!);
|
||||
|
||||
const isActive = $derived(graphState.activeNodeId === node.id);
|
||||
const isSelected = $derived(graphState.selectedNodes.has(node.id));
|
||||
@@ -29,9 +31,18 @@
|
||||
: colors.outline)
|
||||
);
|
||||
|
||||
const sectionHeights = $derived(
|
||||
Object
|
||||
.keys(nodeType.inputs || {})
|
||||
.map(key => getParameterHeight(nodeType, key) / 10)
|
||||
.filter(b => !!b)
|
||||
);
|
||||
|
||||
let meshRef: Mesh | undefined = $state();
|
||||
|
||||
const height = graphState.getNodeHeight(node.type);
|
||||
const height = getNodeHeight(node.state.type!);
|
||||
|
||||
const zoom = $derived(graphState.cameraPosition[2]);
|
||||
|
||||
$effect(() => {
|
||||
if (meshRef && !node.state?.mesh) {
|
||||
@@ -39,6 +50,10 @@
|
||||
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>
|
||||
|
||||
<T.Mesh
|
||||
@@ -47,7 +62,7 @@
|
||||
position.y={0.8}
|
||||
rotation.x={-Math.PI / 2}
|
||||
bind:ref={meshRef}
|
||||
visible={inView && z < 7}
|
||||
visible={inView && zoom < 7}
|
||||
>
|
||||
<T.PlaneGeometry args={[20, height]} radius={1} />
|
||||
<T.ShaderMaterial
|
||||
@@ -57,14 +72,19 @@
|
||||
uniforms={{
|
||||
uColorBright: { value: colors['layer-2'] },
|
||||
uColorDark: { value: colors['layer-1'] },
|
||||
uStrokeColor: { value: colors['layer-2'].clone() },
|
||||
uStrokeWidth: { value: 1.0 },
|
||||
uStrokeColor: { value: colors.outline.clone() },
|
||||
uSectionHeights: { value: [5, 10] },
|
||||
uNumSections: { value: 2 },
|
||||
uWidth: { value: 20 },
|
||||
uHeight: { value: height }
|
||||
uHeight: { value: 200 },
|
||||
uZoom: { value: 1.0 }
|
||||
}}
|
||||
uniforms.uStrokeColor.value={strokeColor.clone()}
|
||||
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
||||
uniforms.uZoom.value={zoomValue}
|
||||
uniforms.uHeight.value={height}
|
||||
uniforms.uSectionHeights.value={sectionHeights}
|
||||
uniforms.uNumSections.value={sectionHeights.length}
|
||||
uniforms.uStrokeColor.value={strokeColor}
|
||||
/>
|
||||
</T.Mesh>
|
||||
|
||||
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} />
|
||||
<NodeHtml bind:node {inView} {isActive} {isSelected} z={zoom} />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { NodeInstance } from '@nodarium/types';
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import type { NodeInstance, Socket } from '@nodarium/types';
|
||||
import { getGraphState } from '../graph-state.svelte';
|
||||
import { createNodePath } from '../helpers/index.js';
|
||||
import { getSocketPosition } from '../helpers/nodeHelpers';
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
@@ -14,7 +16,7 @@
|
||||
graphState.setDownSocket?.({
|
||||
node,
|
||||
index: 0,
|
||||
position: graphState.getSocketPosition?.(node, 0)
|
||||
position: getSocketPosition?.(node, 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -43,14 +45,35 @@
|
||||
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>
|
||||
|
||||
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
|
||||
<div
|
||||
class="wrapper"
|
||||
data-node-id={node.id}
|
||||
data-node-type={node.type}
|
||||
style:--socket-color={hoverColor}
|
||||
class:possible-socket={graphState?.possibleSocketIds.has(socketId)}
|
||||
>
|
||||
<div class="content">
|
||||
{#if appSettings.value.debug.advancedMode}
|
||||
<span class="bg-white text-black! mr-2 px-1 rounded-sm opacity-30">{node.id}</span>
|
||||
{/if}
|
||||
{node.type.split('/').pop()}
|
||||
</div>
|
||||
<div
|
||||
class="click-target"
|
||||
class="target"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onmousedown={handleMouseDown}
|
||||
@@ -78,7 +101,20 @@
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.click-target {
|
||||
.possible-socket .target::before {
|
||||
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;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
@@ -89,7 +125,7 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.click-target:hover + svg path {
|
||||
.target:hover + svg path {
|
||||
d: var(--hover-path);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { NodeInput, NodeInstance } from '@nodarium/types';
|
||||
import type { NodeInput, NodeInstance, Socket } from '@nodarium/types';
|
||||
import { getGraphManager, getGraphState } from '../graph-state.svelte';
|
||||
import { createNodePath } from '../helpers';
|
||||
import { getParameterHeight, getSocketPosition } from '../helpers/nodeHelpers';
|
||||
import NodeInputEl from './NodeInput.svelte';
|
||||
|
||||
type Props = {
|
||||
@@ -12,19 +13,18 @@
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
const inputType = $derived(node?.state?.type?.inputs?.[id]);
|
||||
const nodeType = $derived(node.state.type!);
|
||||
|
||||
const inputType = $derived(nodeType.inputs?.[id]);
|
||||
|
||||
const socketId = $derived(`${node.id}-${id}`);
|
||||
const isShape = $derived(input.type === 'shape' && input.external !== true);
|
||||
const height = $derived(isShape ? 200 : 100);
|
||||
|
||||
const graphState = getGraphState();
|
||||
const graphId = graph?.id;
|
||||
|
||||
const elementId = `input-${Math.random().toString(36).substring(7)}`;
|
||||
const height = $derived(getParameterHeight(nodeType, id));
|
||||
|
||||
function handleMouseDown(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
@@ -32,18 +32,18 @@
|
||||
graphState.setDownSocket({
|
||||
node,
|
||||
index: id,
|
||||
position: graphState.getSocketPosition?.(node, id)
|
||||
position: getSocketPosition(node, id)
|
||||
});
|
||||
}
|
||||
|
||||
const leftBump = $derived(node.state?.type?.inputs?.[id].internal !== true);
|
||||
const leftBump = $derived(nodeType.inputs?.[id].internal !== true);
|
||||
const cornerBottom = $derived(isLast ? 5 : 0);
|
||||
const aspectRatio = 0.5;
|
||||
|
||||
const path = $derived(
|
||||
createNodePath({
|
||||
depth: 6,
|
||||
height: 18,
|
||||
height: 2000 / height,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
@@ -53,13 +53,24 @@
|
||||
const pathHover = $derived(
|
||||
createNodePath({
|
||||
depth: 7,
|
||||
height: 20,
|
||||
height: 2200 / height,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio
|
||||
})
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
@@ -67,6 +78,7 @@
|
||||
data-node-type={node.type}
|
||||
data-node-input={id}
|
||||
style:height="{height}px"
|
||||
style:--socket-color={hoverColor}
|
||||
class:possible-socket={graphState?.possibleSocketIds.has(socketId)}
|
||||
>
|
||||
{#key id && graphId}
|
||||
@@ -74,10 +86,6 @@
|
||||
{#if inputType?.label !== ''}
|
||||
<label for={elementId} title={input.description}>{input.label || id}</label>
|
||||
{/if}
|
||||
<span
|
||||
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
|
||||
title={JSON.stringify(input, null, 2)}
|
||||
></span>
|
||||
{#if inputType?.external !== true}
|
||||
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
|
||||
{/if}
|
||||
@@ -99,10 +107,8 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
style={`
|
||||
--path: path("${path}");
|
||||
--hover-path: path("${pathHover}");
|
||||
`}
|
||||
style:--path={`path("${path}")`}
|
||||
style:--hover-path={`path("${pathHover}")`}
|
||||
>
|
||||
<path vector-effect="non-scaling-stroke"></path>
|
||||
</svg>
|
||||
@@ -124,9 +130,16 @@
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
}
|
||||
|
||||
.possible-socket .target {
|
||||
box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
.possible-socket .target::before {
|
||||
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.5;
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
@@ -136,11 +149,12 @@
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-inline: 20px;
|
||||
height: 100%;
|
||||
justify-content: space-around;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
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> {
|
||||
value = $state<T>() as T;
|
||||
key = '';
|
||||
@@ -10,7 +42,10 @@ export class LocalStore<T> {
|
||||
|
||||
if (browser) {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item) this.value = this.deserialize(item);
|
||||
if (item) {
|
||||
const storedValue = this.deserialize(item);
|
||||
this.value = mergeRecursive(storedValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
$effect.root(() => {
|
||||
|
||||
11
app/src/lib/node-registry/debugNode.ts
Normal file
11
app/src/lib/node-registry/debugNode.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const debugNode = {
|
||||
id: 'max/plantarium/debug',
|
||||
inputs: {
|
||||
input: {
|
||||
type: '*'
|
||||
}
|
||||
},
|
||||
execute(_data: Int32Array): Int32Array {
|
||||
return _data;
|
||||
}
|
||||
} as const;
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
type AsyncCache,
|
||||
type NodeDefinition,
|
||||
NodeDefinitionSchema,
|
||||
type NodeId,
|
||||
type NodeRegistry
|
||||
} from '@nodarium/types';
|
||||
import { createLogger, createWasmWrapper } from '@nodarium/utils';
|
||||
@@ -12,11 +13,19 @@ log.mute();
|
||||
export class RemoteNodeRegistry implements NodeRegistry {
|
||||
status: 'loading' | 'ready' | 'error' = 'loading';
|
||||
private nodes: Map<string, NodeDefinition> = new Map();
|
||||
private memory = new WebAssembly.Memory({ initial: 1024, maximum: 8192 });
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
public cache?: AsyncCache<ArrayBuffer | string>
|
||||
) {}
|
||||
public cache?: AsyncCache<ArrayBuffer | string>,
|
||||
nodes?: NodeDefinition[]
|
||||
) {
|
||||
if (nodes?.length) {
|
||||
for (const node of nodes) {
|
||||
this.nodes.set(node.id, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchJson(url: string, skipCache = false) {
|
||||
const finalUrl = `${this.url}/${url}`;
|
||||
@@ -163,6 +172,13 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
||||
}
|
||||
|
||||
getAllNodes() {
|
||||
return [...this.nodes.values()];
|
||||
const allNodes = [...this.nodes.values()];
|
||||
log.info('getting all nodes', allNodes);
|
||||
return allNodes;
|
||||
}
|
||||
|
||||
async overwriteNode(nodeId: NodeId, node: NodeDefinition) {
|
||||
log.info('Overwritten node', { nodeId, node });
|
||||
this.nodes.set(nodeId, node);
|
||||
}
|
||||
}
|
||||
|
||||
19
app/src/lib/result-viewer/Debug.svelte
Normal file
19
app/src/lib/result-viewer/Debug.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { T } from '@threlte/core';
|
||||
import type { Group } from 'three';
|
||||
import { updateDebugScene } from './debug';
|
||||
|
||||
type Props = {
|
||||
debugData?: Record<number, { type: string; data: Int32Array }>;
|
||||
};
|
||||
|
||||
let group = $state<Group>(null!);
|
||||
const { debugData }: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (!group || !debugData) return;
|
||||
updateDebugScene(group, $state.snapshot(debugData));
|
||||
});
|
||||
</script>
|
||||
|
||||
<T.Group bind:ref={group} />
|
||||
@@ -1,33 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { colors } from '$lib/graph-interface/graph/colors.svelte';
|
||||
import { T, useTask, useThrelte } from '@threlte/core';
|
||||
import { Grid, MeshLineGeometry, MeshLineMaterial, Text } from '@threlte/extras';
|
||||
import {
|
||||
Box3,
|
||||
type BufferGeometry,
|
||||
type Group,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Vector3,
|
||||
type Vector3Tuple
|
||||
} from 'three';
|
||||
import { Grid } from '@threlte/extras';
|
||||
import { Box3, type BufferGeometry, type Group, Mesh, MeshBasicMaterial, Vector3 } from 'three';
|
||||
import { appSettings } from '../settings/app-settings.svelte';
|
||||
import Camera from './Camera.svelte';
|
||||
import Debug from './Debug.svelte';
|
||||
|
||||
const { renderStage, invalidate: _invalidate } = useThrelte();
|
||||
|
||||
type Props = {
|
||||
fps: number[];
|
||||
lines: Vector3[][];
|
||||
debugData?: Record<number, { type: string; data: Int32Array }>;
|
||||
scene: Group;
|
||||
centerCamera: boolean;
|
||||
};
|
||||
|
||||
let {
|
||||
lines,
|
||||
centerCamera,
|
||||
fps = $bindable(),
|
||||
scene = $bindable()
|
||||
scene = $bindable(),
|
||||
debugData
|
||||
}: Props = $props();
|
||||
|
||||
let geometries = $state.raw<BufferGeometry[]>([]);
|
||||
@@ -91,18 +84,12 @@
|
||||
});
|
||||
_invalidate();
|
||||
});
|
||||
|
||||
function getPosition(geo: BufferGeometry, i: number) {
|
||||
return [
|
||||
geo.attributes.position.array[i],
|
||||
geo.attributes.position.array[i + 1],
|
||||
geo.attributes.position.array[i + 2]
|
||||
] as Vector3Tuple;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Camera {center} {centerCamera} />
|
||||
|
||||
<Debug {debugData} />
|
||||
|
||||
{#if appSettings.value.showGrid}
|
||||
<Grid
|
||||
cellColor={colors['outline']}
|
||||
@@ -116,35 +103,4 @@
|
||||
fadeOrigin={new Vector3(0, 0, 0)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<T.Group>
|
||||
{#if geometries}
|
||||
{#each geometries as geo (geo.id)}
|
||||
{#if appSettings.value.debug.showIndices}
|
||||
{#each geo.attributes.position.array, i (i)}
|
||||
{#if i % 3 === 0}
|
||||
<Text fontSize={0.25} position={getPosition(geo, i)} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if appSettings.value.debug.showVertices}
|
||||
<T.Points visible={true}>
|
||||
<T is={geo} />
|
||||
<T.PointsMaterial size={0.25} />
|
||||
</T.Points>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<T.Group bind:ref={scene}></T.Group>
|
||||
</T.Group>
|
||||
|
||||
{#if appSettings.value.debug.showStemLines && lines}
|
||||
{#each lines as line (line[0].x + '-' + line[0].y + '-' + '' + line[0].z)}
|
||||
<T.Mesh>
|
||||
<MeshLineGeometry points={line} />
|
||||
<MeshLineMaterial width={0.1} color="red" depthTest={false} />
|
||||
</T.Mesh>
|
||||
{/each}
|
||||
{/if}
|
||||
<T.Group bind:ref={scene}></T.Group>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import SmallPerformanceViewer from '$lib/performance/SmallPerformanceViewer.svelte';
|
||||
import { appSettings } from '$lib/settings/app-settings.svelte';
|
||||
import { decodeFloat, splitNestedArray } from '@nodarium/utils';
|
||||
import { splitNestedArray } from '@nodarium/utils';
|
||||
import type { PerformanceStore } from '@nodarium/utils';
|
||||
import { Canvas } from '@threlte/core';
|
||||
import { DoubleSide, Vector3 } from 'three';
|
||||
import { DoubleSide } from 'three';
|
||||
import { type Group, MeshMatcapMaterial, TextureLoader } from 'three';
|
||||
import { createGeometryPool, createInstancedGeometryPool } from './geometryPool';
|
||||
import Scene from './Scene.svelte';
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
let geometryPool: ReturnType<typeof createGeometryPool>;
|
||||
let instancePool: ReturnType<typeof createInstancedGeometryPool>;
|
||||
|
||||
export function updateGeometries(inputs: Int32Array[], group: Group) {
|
||||
geometryPool = geometryPool || createGeometryPool(group, material);
|
||||
instancePool = instancePool || createInstancedGeometryPool(group, material);
|
||||
@@ -40,44 +41,16 @@
|
||||
scene: Group;
|
||||
centerCamera: boolean;
|
||||
perf: PerformanceStore;
|
||||
debugData?: Record<number, { type: string; data: Int32Array }>;
|
||||
};
|
||||
|
||||
let { scene = $bindable(), centerCamera, perf }: Props = $props();
|
||||
|
||||
let lines = $state<Vector3[][]>([]);
|
||||
|
||||
function createLineGeometryFromEncodedData(encodedData: Int32Array) {
|
||||
const positions: Vector3[] = [];
|
||||
|
||||
const amount = (encodedData.length - 1) / 4;
|
||||
|
||||
for (let i = 0; i < amount; i++) {
|
||||
const x = decodeFloat(encodedData[2 + i * 4 + 0]);
|
||||
const y = decodeFloat(encodedData[2 + i * 4 + 1]);
|
||||
const z = decodeFloat(encodedData[2 + i * 4 + 2]);
|
||||
positions.push(new Vector3(x, y, z));
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
let { scene = $bindable(), centerCamera, debugData, perf }: Props = $props();
|
||||
|
||||
export const update = function update(result: Int32Array) {
|
||||
perf.addPoint('split-result');
|
||||
const inputs = splitNestedArray(result);
|
||||
perf.endPoint();
|
||||
|
||||
if (appSettings.value.debug.showStemLines) {
|
||||
perf.addPoint('create-lines');
|
||||
lines = inputs
|
||||
.map((input) => {
|
||||
if (input[0] === 0) {
|
||||
return createLineGeometryFromEncodedData(input);
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as Vector3[][];
|
||||
perf.endPoint();
|
||||
}
|
||||
|
||||
perf.addPoint('update-geometries');
|
||||
|
||||
const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
|
||||
@@ -89,7 +62,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if appSettings.value.debug.showPerformancePanel}
|
||||
{#if appSettings.value.debug.advancedMode}
|
||||
<SmallPerformanceViewer {fps} store={perf} />
|
||||
{/if}
|
||||
|
||||
@@ -97,8 +70,8 @@
|
||||
<Canvas>
|
||||
<Scene
|
||||
bind:this={sceneComponent}
|
||||
{lines}
|
||||
{centerCamera}
|
||||
{debugData}
|
||||
bind:scene
|
||||
bind:fps
|
||||
/>
|
||||
|
||||
90
app/src/lib/result-viewer/debug.ts
Normal file
90
app/src/lib/result-viewer/debug.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { splitNestedArray } from '@nodarium/utils';
|
||||
import {
|
||||
BufferGeometry,
|
||||
type Group,
|
||||
InstancedMesh,
|
||||
Line,
|
||||
LineBasicMaterial,
|
||||
Matrix4,
|
||||
MeshBasicMaterial,
|
||||
SphereGeometry,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
function writePath(scene: Group, data: Int32Array): Vector3[] {
|
||||
const positions: Vector3[] = [];
|
||||
const f32 = new Float32Array(data.buffer);
|
||||
|
||||
for (let i = 2; i + 2 < f32.length; i += 4) {
|
||||
const vec = new Vector3(f32[i], f32[i + 1], f32[i + 2]);
|
||||
positions.push(vec);
|
||||
}
|
||||
|
||||
// Path line
|
||||
if (positions.length >= 2) {
|
||||
const geometry = new BufferGeometry().setFromPoints(positions);
|
||||
const line = new Line(
|
||||
geometry,
|
||||
new LineBasicMaterial({ color: 0xff0000, depthTest: false })
|
||||
);
|
||||
scene.add(line);
|
||||
}
|
||||
|
||||
// Instanced spheres at points
|
||||
if (positions.length > 0) {
|
||||
const sphereGeometry = new SphereGeometry(0.05, 8, 8); // keep low-poly
|
||||
const sphereMaterial = new MeshBasicMaterial({
|
||||
color: 0xff0000,
|
||||
depthTest: false
|
||||
});
|
||||
|
||||
const spheres = new InstancedMesh(
|
||||
sphereGeometry,
|
||||
sphereMaterial,
|
||||
positions.length
|
||||
);
|
||||
|
||||
const matrix = new Matrix4();
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
matrix.makeTranslation(
|
||||
positions[i].x,
|
||||
positions[i].y,
|
||||
positions[i].z
|
||||
);
|
||||
spheres.setMatrixAt(i, matrix);
|
||||
}
|
||||
|
||||
spheres.instanceMatrix.needsUpdate = true;
|
||||
scene.add(spheres);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
function clearGroup(group: Group) {
|
||||
for (let i = group.children.length - 1; i >= 0; i--) {
|
||||
const child = group.children[i];
|
||||
group.remove(child);
|
||||
// optional but correct: free GPU memory
|
||||
// @ts-expect-error three.js runtime fields
|
||||
child.geometry?.dispose?.();
|
||||
// @ts-expect-error three.js runtime fields
|
||||
child.material?.dispose?.();
|
||||
}
|
||||
}
|
||||
|
||||
export function updateDebugScene(
|
||||
group: Group,
|
||||
data: Record<number, { type: string; data: Int32Array }>
|
||||
) {
|
||||
clearGroup(group);
|
||||
return Object.entries(data || {}).map(([, d]) => {
|
||||
switch (d.type) {
|
||||
case 'path':
|
||||
splitNestedArray(d.data)
|
||||
.forEach(p => writePath(group, p));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return (_g: Group) => {};
|
||||
}).flat();
|
||||
}
|
||||
39
app/src/lib/runtime/helpers.ts
Normal file
39
app/src/lib/runtime/helpers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export function logInt32ArrayChanges(
|
||||
before: Int32Array,
|
||||
after: Int32Array,
|
||||
clamp = 10
|
||||
): void {
|
||||
if (before.length !== after.length) {
|
||||
throw new Error('Arrays must have the same length');
|
||||
}
|
||||
|
||||
let rangeStart: number | null = null;
|
||||
let collected: number[] = [];
|
||||
|
||||
const flush = (endIndex: number) => {
|
||||
if (rangeStart === null) return;
|
||||
|
||||
const preview = collected.slice(0, clamp);
|
||||
const suffix = collected.length > clamp ? '...' : '';
|
||||
|
||||
console.log(
|
||||
`Change ${rangeStart}-${endIndex}: [${preview.join(', ')}${suffix}]`
|
||||
);
|
||||
|
||||
rangeStart = null;
|
||||
collected = [];
|
||||
};
|
||||
|
||||
for (let i = 0; i < before.length; i++) {
|
||||
if (before[i] !== after[i]) {
|
||||
if (rangeStart === null) {
|
||||
rangeStart = i;
|
||||
}
|
||||
collected.push(after[i]);
|
||||
} else {
|
||||
flush(i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
flush(before.length - 1);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { SettingsToStore } from '$lib/settings/app-settings.svelte';
|
||||
import { RemoteNodeRegistry } from '@nodarium/registry';
|
||||
import type {
|
||||
Graph,
|
||||
NodeDefinition,
|
||||
@@ -7,24 +9,38 @@ import type {
|
||||
SyncCache
|
||||
} from '@nodarium/types';
|
||||
import {
|
||||
concatEncodedArrays,
|
||||
createLogger,
|
||||
createWasmWrapper,
|
||||
encodeFloat,
|
||||
fastHashArrayBuffer,
|
||||
type PerformanceStore
|
||||
} from '@nodarium/utils';
|
||||
import { DevSettingsType } from '../../routes/dev/settings.svelte';
|
||||
import { logInt32ArrayChanges } from './helpers';
|
||||
import type { RuntimeNode } from './types';
|
||||
|
||||
const log = createLogger('runtime-executor');
|
||||
log.mute();
|
||||
// log.mute(); // Keep logging enabled for debug info
|
||||
|
||||
function getValue(input: NodeInput, value?: unknown) {
|
||||
const remoteRegistry = new RemoteNodeRegistry('');
|
||||
|
||||
type WasmExecute = (outputPos: number, args: number[]) => number;
|
||||
|
||||
function getValue(input: NodeInput, value?: unknown): number | number[] | Int32Array {
|
||||
if (value === undefined && 'value' in input) {
|
||||
value = input.value;
|
||||
}
|
||||
|
||||
if (input.type === 'float') {
|
||||
return encodeFloat(value as number);
|
||||
switch (input.type) {
|
||||
case 'float':
|
||||
return encodeFloat(value as number);
|
||||
|
||||
case 'select':
|
||||
return (value as number) ?? 0;
|
||||
|
||||
case 'vec3': {
|
||||
const arr = Array.isArray(value) ? value : [];
|
||||
return [0, arr.length + 1, ...arr.map(v => encodeFloat(v)), 1, 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
@@ -40,57 +56,82 @@ function getValue(input: NodeInput, value?: unknown) {
|
||||
return [0, value.length + 1, ...value, 1, 1] as number[];
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
if (typeof value === 'boolean') return value ? 1 : 0;
|
||||
if (typeof value === 'number') return value;
|
||||
if (value instanceof Int32Array) return value;
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value instanceof Int32Array) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown input type ${input.type}`);
|
||||
throw new Error(`Unsupported input type: ${input.type}`);
|
||||
}
|
||||
|
||||
export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
private definitionMap: Map<string, NodeDefinition> = new Map();
|
||||
function compareInt32(a: Int32Array, b: Int32Array): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export type Pointer = {
|
||||
start: number;
|
||||
end: number;
|
||||
_title?: string;
|
||||
};
|
||||
|
||||
private seed = Math.floor(Math.random() * 100000000);
|
||||
private debugData: Record<number, { type: string; data: Int32Array }> = {};
|
||||
|
||||
perf?: PerformanceStore;
|
||||
|
||||
constructor(
|
||||
private registry: NodeRegistry,
|
||||
private readonly registry: NodeRegistry,
|
||||
public cache?: SyncCache<Int32Array>
|
||||
) {
|
||||
this.cache = undefined;
|
||||
this.refreshView();
|
||||
log.info('MemoryRuntimeExecutor initialized');
|
||||
}
|
||||
|
||||
private refreshView(): void {
|
||||
this.memoryView = new Int32Array(this.memory.buffer);
|
||||
log.info(`Memory view refreshed, length: ${this.memoryView.length}`);
|
||||
}
|
||||
|
||||
public getMemory(): Int32Array {
|
||||
return new Int32Array(this.memory.buffer);
|
||||
}
|
||||
|
||||
private map = new Map<string, { definition: NodeDefinition; execute: WasmExecute }>();
|
||||
private async getNodeDefinitions(graph: Graph) {
|
||||
if (this.registry.status !== 'ready') {
|
||||
throw new Error('Node registry is not ready');
|
||||
}
|
||||
|
||||
await this.registry.load(graph.nodes.map((node) => node.type));
|
||||
await this.registry.load(graph.nodes.map(n => n.type));
|
||||
log.info(`Loaded ${graph.nodes.length} node types from registry`);
|
||||
|
||||
const typeMap = new Map<string, NodeDefinition>();
|
||||
for (const node of graph.nodes) {
|
||||
if (!typeMap.has(node.type)) {
|
||||
const type = this.registry.getNode(node.type);
|
||||
if (type) {
|
||||
typeMap.set(node.type, type);
|
||||
}
|
||||
}
|
||||
for (const { type } of graph.nodes) {
|
||||
if (this.map.has(type)) continue;
|
||||
|
||||
const def = this.registry.getNode(type);
|
||||
if (!def) continue;
|
||||
|
||||
log.info(`Fetching WASM for node type: ${type}`);
|
||||
const buffer = await remoteRegistry.fetchArrayBuffer(`nodes/${type}.wasm`);
|
||||
const wrapper = createWasmWrapper(buffer, this.memory);
|
||||
|
||||
this.map.set(type, {
|
||||
definition: def,
|
||||
execute: wrapper.execute
|
||||
});
|
||||
log.info(`Node type ${type} loaded and wrapped`);
|
||||
}
|
||||
return typeMap;
|
||||
|
||||
return this.map;
|
||||
}
|
||||
|
||||
private async addMetaData(graph: Graph) {
|
||||
// First, lets check if all nodes have a definition
|
||||
this.definitionMap = await this.getNodeDefinitions(graph);
|
||||
this.nodes = await this.getNodeDefinitions(graph);
|
||||
log.info(`Metadata added for ${this.nodes.size} nodes`);
|
||||
|
||||
const graphNodes = graph.nodes.map(node => {
|
||||
const n = node as RuntimeNode;
|
||||
@@ -103,55 +144,68 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
return n;
|
||||
});
|
||||
|
||||
const outputNode = graphNodes.find((node) => node.type.endsWith('/output'));
|
||||
if (!outputNode) {
|
||||
throw new Error('No output node found');
|
||||
}
|
||||
const outputNode = graphNodes.find(n => n.type.endsWith('/output') || n.type.endsWith('/debug'))
|
||||
?? graphNodes[0];
|
||||
|
||||
const nodeMap = new Map(
|
||||
graphNodes.map((node) => [node.id, node])
|
||||
);
|
||||
const nodeMap = new Map(graphNodes.map(n => [n.id, n]));
|
||||
|
||||
// loop through all edges and assign the parent and child nodes to each node
|
||||
for (const edge of graph.edges) {
|
||||
const [parentId, /*_parentOutput*/, childId, childInput] = edge;
|
||||
const parent = nodeMap.get(parentId);
|
||||
const child = nodeMap.get(childId);
|
||||
if (parent && child) {
|
||||
parent.state.children.push(child);
|
||||
child.state.parents.push(parent);
|
||||
child.state.inputNodes[childInput] = parent;
|
||||
}
|
||||
if (!parent || !child) continue;
|
||||
|
||||
parent.state.children.push(child);
|
||||
child.state.parents.push(parent);
|
||||
child.state.inputNodes[childInput] = parent;
|
||||
}
|
||||
|
||||
const nodes = [];
|
||||
const nodes = new Map<number, RuntimeNode>();
|
||||
|
||||
// loop through all the nodes and assign each nodes its depth
|
||||
const stack = [outputNode];
|
||||
const stack = [outputNode, ...graphNodes.filter(n => n.type.endsWith('/debug'))];
|
||||
while (stack.length) {
|
||||
const node = stack.pop();
|
||||
if (!node) continue;
|
||||
const node = stack.pop()!;
|
||||
for (const parent of node.state.parents) {
|
||||
parent.state = parent.state || {};
|
||||
parent.state.depth = node.state.depth + 1;
|
||||
stack.push(parent);
|
||||
}
|
||||
nodes.push(node);
|
||||
nodes.set(node.id, node);
|
||||
}
|
||||
|
||||
return [outputNode, nodes] as const;
|
||||
for (const node of graphNodes) {
|
||||
if (node.type.endsWith('/debug')) {
|
||||
node.state = node.state || {};
|
||||
const parent = node.state.parents[0];
|
||||
if (parent) {
|
||||
node.state.depth = parent.state.depth - 1;
|
||||
parent.state.debugNode = true;
|
||||
}
|
||||
nodes.set(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
const _nodes = [...nodes.values()];
|
||||
|
||||
return [outputNode, _nodes] as const;
|
||||
}
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
this.perf?.addPoint('runtime');
|
||||
private writeToMemory(value: number | number[] | Int32Array, title?: string): Pointer {
|
||||
const start = this.offset;
|
||||
|
||||
if (typeof value === 'number') {
|
||||
this.memoryView[this.offset++] = value;
|
||||
} else {
|
||||
this.memoryView.set(value, this.offset);
|
||||
this.offset += value.length;
|
||||
}
|
||||
|
||||
let a = performance.now();
|
||||
this.debugData = {};
|
||||
|
||||
// Then we add some metadata to the graph
|
||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
||||
let b = performance.now();
|
||||
|
||||
this.perf?.addPoint('collect-metadata', b - a);
|
||||
const [_outputNode, nodes] = await this.addMetaData(graph);
|
||||
|
||||
/*
|
||||
* Here we sort the nodes into buckets, which we then execute one by one
|
||||
@@ -169,58 +223,75 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
(a, b) => (b.state?.depth || 0) - (a.state?.depth || 0)
|
||||
);
|
||||
|
||||
// here we store the intermediate results of the nodes
|
||||
const results: Record<string, Int32Array> = {};
|
||||
console.log({ settings });
|
||||
|
||||
if (settings['randomSeed']) {
|
||||
this.seed = Math.floor(Math.random() * 100000000);
|
||||
}
|
||||
this.printMemory();
|
||||
const seedPtr = this.writeToMemory(this.seed, 'seed');
|
||||
|
||||
const settingPtrs = new Map<string, Pointer>(
|
||||
Object.entries(settings).map((
|
||||
[key, value]
|
||||
) => [key as string, this.writeToMemory(value as number, `setting.${key}`)])
|
||||
);
|
||||
|
||||
for (const node of sortedNodes) {
|
||||
const node_type = this.definitionMap.get(node.type)!;
|
||||
const node_type = this.nodes.get(node.type)!;
|
||||
|
||||
console.log('---------------');
|
||||
console.log('STARTING NODE EXECUTION', node_type.definition.id + '/' + node.id);
|
||||
this.printMemory();
|
||||
|
||||
// console.log(node_type.definition.inputs);
|
||||
const inputs = Object.entries(node_type.definition.inputs || {}).map(
|
||||
([key, input]) => {
|
||||
// We should probably initially write this to memory
|
||||
if (input.type === 'seed') {
|
||||
return seedPtr;
|
||||
}
|
||||
|
||||
const title = `${node.id}.${key}`;
|
||||
|
||||
// We should probably initially write this to memory
|
||||
// If the input is linked to a setting, we use that value
|
||||
// TODO: handle nodes which reference undefined settings
|
||||
if (input.setting) {
|
||||
return settingPtrs.get(input.setting)!;
|
||||
}
|
||||
|
||||
// check if the input is connected to another node
|
||||
const inputNode = node.state.inputNodes[key];
|
||||
if (inputNode) {
|
||||
if (this.results[inputNode.id] === undefined) {
|
||||
throw new Error(
|
||||
`Node ${node.type}/${node.id} is missing input from node ${inputNode.type}/${inputNode.id}`
|
||||
);
|
||||
}
|
||||
return this.results[inputNode.id];
|
||||
}
|
||||
|
||||
// If the value is stored in the node itself, we use that value
|
||||
if (node.props?.[key] !== undefined) {
|
||||
const value = getValue(input, node.props[key]);
|
||||
console.log(`Writing prop for ${node.id} -> ${key} to memory`, node.props[key], value);
|
||||
return this.writeToMemory(value, title);
|
||||
}
|
||||
|
||||
return this.writeToMemory(getValue(input), title);
|
||||
}
|
||||
);
|
||||
|
||||
this.printMemory();
|
||||
|
||||
if (!node_type || !node.state || !node_type.execute) {
|
||||
log.warn(`Node ${node.id} has no definition`);
|
||||
continue;
|
||||
}
|
||||
|
||||
a = performance.now();
|
||||
|
||||
// Collect the inputs for the node
|
||||
const inputs = Object.entries(node_type.inputs || {}).map(
|
||||
([key, input]) => {
|
||||
if (input.type === 'seed') {
|
||||
return this.seed;
|
||||
}
|
||||
|
||||
// If the input is linked to a setting, we use that value
|
||||
if (input.setting) {
|
||||
return getValue(input, settings[input.setting]);
|
||||
}
|
||||
|
||||
// check if the input is connected to another node
|
||||
const inputNode = node.state.inputNodes[key];
|
||||
if (inputNode) {
|
||||
if (results[inputNode.id] === undefined) {
|
||||
throw new Error(
|
||||
`Node ${node.type} is missing input from node ${inputNode.type}`
|
||||
);
|
||||
}
|
||||
return results[inputNode.id];
|
||||
}
|
||||
|
||||
// If the value is stored in the node itself, we use that value
|
||||
if (node.props?.[key] !== undefined) {
|
||||
return getValue(input, node.props[key]);
|
||||
}
|
||||
|
||||
return getValue(input);
|
||||
}
|
||||
);
|
||||
b = performance.now();
|
||||
|
||||
this.perf?.addPoint('collected-inputs', b - a);
|
||||
this.inputPtrs[node.id] = inputs;
|
||||
const args = inputs.map(s => [s.start, s.end]).flat();
|
||||
console.log('ARGS', inputs);
|
||||
|
||||
this.printMemory();
|
||||
try {
|
||||
a = performance.now();
|
||||
const encoded_inputs = concatEncodedArrays(inputs);
|
||||
@@ -237,6 +308,12 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
log.log(`Using cached value for ${node_type.id || node.id}`);
|
||||
this.perf?.addPoint('cache-hit', 1);
|
||||
results[node.id] = cachedValue as Int32Array;
|
||||
if (node.state.debugNode && node_type.outputs) {
|
||||
this.debugData[node.id] = {
|
||||
type: node_type.outputs[0],
|
||||
data: cachedValue
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
this.perf?.addPoint('cache-hit', 0);
|
||||
@@ -245,32 +322,152 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
log.log(`Inputs:`, inputs);
|
||||
a = performance.now();
|
||||
results[node.id] = node_type.execute(encoded_inputs);
|
||||
if (node.state.debugNode && node_type.outputs) {
|
||||
this.debugData[node.id] = {
|
||||
type: node_type.outputs[0],
|
||||
data: results[node.id]
|
||||
};
|
||||
}
|
||||
log.log('Executed', node.type, node.id);
|
||||
b = performance.now();
|
||||
|
||||
if (this.cache && node.id !== outputNode.id) {
|
||||
this.cache.set(inputHash, results[node.id]);
|
||||
this.cache.set(inputHash, this.results[node.id]);
|
||||
}
|
||||
|
||||
this.perf?.addPoint('node/' + node_type.id, b - a);
|
||||
log.log('Result:', results[node.id]);
|
||||
log.groupEnd();
|
||||
} catch (e) {
|
||||
log.groupEnd();
|
||||
log.error(`Error executing node ${node_type.id || node.id}`, e);
|
||||
console.error(`Failed to execute node ${node.type}/${node.id}`, e);
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// return the result of the parent of the output node
|
||||
const res = results[outputNode.id];
|
||||
this.isRunning = true;
|
||||
log.info('Execution started');
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.size = sortedNodes.length * 2;
|
||||
try {
|
||||
this.offset = 0;
|
||||
this.results = {};
|
||||
this.inputPtrs = {};
|
||||
this.allPtrs = [];
|
||||
this.seed += 2;
|
||||
|
||||
this.refreshView();
|
||||
|
||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
||||
|
||||
const sortedNodes = [...nodes].sort(
|
||||
(a, b) => (b.state.depth ?? 0) - (a.state.depth ?? 0)
|
||||
);
|
||||
|
||||
const seedPtr = this.writeToMemory(this.seed, 'seed');
|
||||
|
||||
const settingPtrs = new Map<string, Pointer>();
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
const ptr = this.writeToMemory(value as number, `setting.${key}`);
|
||||
settingPtrs.set(key, ptr);
|
||||
}
|
||||
|
||||
let lastNodePtr: Pointer | undefined = undefined;
|
||||
|
||||
for (const node of sortedNodes) {
|
||||
const nodeType = this.nodes.get(node.type);
|
||||
if (!nodeType) continue;
|
||||
|
||||
log.info(`Executing node: ${node.id} (type: ${node.type})`);
|
||||
|
||||
const inputs = Object.entries(nodeType.definition.inputs || {}).map(
|
||||
([key, input]) => {
|
||||
if (input.type === 'seed') return seedPtr;
|
||||
|
||||
if (input.setting) {
|
||||
const ptr = settingPtrs.get(input.setting);
|
||||
if (!ptr) throw new Error(`Missing setting: ${input.setting}`);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
const src = node.state.inputNodes[key];
|
||||
if (src) {
|
||||
const res = this.results[src.id];
|
||||
if (!res) {
|
||||
throw new Error(`Missing input from ${src.type}/${src.id}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (node.props?.[key] !== undefined) {
|
||||
return this.writeToMemory(
|
||||
getValue(input, node.props[key]),
|
||||
`${node.id}.${key}`
|
||||
);
|
||||
}
|
||||
|
||||
return this.writeToMemory(getValue(input), `${node.id}.${key}`);
|
||||
}
|
||||
);
|
||||
|
||||
this.inputPtrs[node.id] = inputs;
|
||||
const args = inputs.flatMap(p => [p.start * 4, p.end * 4]);
|
||||
|
||||
log.info(`Executing node ${node.type}/${node.id}`);
|
||||
const memoryBefore = this.memoryView.slice(0, this.offset);
|
||||
const bytesWritten = nodeType.execute(this.offset * 4, args);
|
||||
this.refreshView();
|
||||
const memoryAfter = this.memoryView.slice(0, this.offset);
|
||||
logInt32ArrayChanges(memoryBefore, memoryAfter);
|
||||
this.refreshView();
|
||||
|
||||
const outLen = bytesWritten >> 2;
|
||||
const outputStart = this.offset;
|
||||
|
||||
if (
|
||||
args.length === 2
|
||||
&& inputs[0].end - inputs[0].start === outLen
|
||||
&& compareInt32(
|
||||
this.memoryView.slice(inputs[0].start, inputs[0].end),
|
||||
this.memoryView.slice(outputStart, outputStart + outLen)
|
||||
)
|
||||
) {
|
||||
this.results[node.id] = inputs[0];
|
||||
this.allPtrs.push(this.results[node.id]);
|
||||
log.info(`Node ${node.id} result reused input memory`);
|
||||
} else {
|
||||
this.results[node.id] = {
|
||||
start: outputStart,
|
||||
end: outputStart + outLen,
|
||||
_title: `${node.id} ->`
|
||||
};
|
||||
this.allPtrs.push(this.results[node.id]);
|
||||
this.offset += outLen;
|
||||
lastNodePtr = this.results[node.id];
|
||||
log.info(
|
||||
`Node ${node.id} wrote result to memory: start=${outputStart}, end=${outputStart + outLen
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const res = this.results[outputNode.id] ?? lastNodePtr;
|
||||
if (!res) throw new Error('Output node produced no result');
|
||||
|
||||
log.info(`Execution finished, output pointer: start=${res.start}, end=${res.end}`);
|
||||
this.refreshView();
|
||||
return this.memoryView.slice(res.start, res.end);
|
||||
} catch (e) {
|
||||
log.info('Execution error:', e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
console.log('Final Memory', [...this.memoryView.slice(0, 20)]);
|
||||
this.perf?.endPoint('runtime');
|
||||
log.info('Executor state reset');
|
||||
}
|
||||
}
|
||||
|
||||
this.perf?.endPoint('runtime');
|
||||
|
||||
return res as unknown as Int32Array;
|
||||
getDebugData() {
|
||||
return this.debugData;
|
||||
}
|
||||
|
||||
getPerformanceData() {
|
||||
|
||||
@@ -5,6 +5,7 @@ type RuntimeState = {
|
||||
parents: RuntimeNode[];
|
||||
children: RuntimeNode[];
|
||||
inputNodes: Record<string, RuntimeNode>;
|
||||
debugNode?: boolean;
|
||||
};
|
||||
|
||||
export type RuntimeNode = SerializedNode & { state: RuntimeState };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { debugNode } from '$lib/node-registry/debugNode';
|
||||
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import { createPerformanceStore } from '@nodarium/utils';
|
||||
@@ -5,7 +6,7 @@ import { MemoryRuntimeExecutor } from './runtime-executor';
|
||||
import { MemoryRuntimeCache } from './runtime-executor-cache';
|
||||
|
||||
const indexDbCache = new IndexDBCache('node-registry');
|
||||
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache);
|
||||
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache, [debugNode]);
|
||||
|
||||
const cache = new MemoryRuntimeCache();
|
||||
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
|
||||
@@ -43,3 +44,7 @@ export async function executeGraph(
|
||||
export function getPerformanceData() {
|
||||
return performanceStore.get();
|
||||
}
|
||||
|
||||
export function getDebugData() {
|
||||
return executor.getDebugData();
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
|
||||
new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)
|
||||
);
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
return this.worker.executeGraph(graph, settings);
|
||||
}
|
||||
async getPerformanceData() {
|
||||
getPerformanceData() {
|
||||
return this.worker.getPerformanceData();
|
||||
}
|
||||
getDebugData() {
|
||||
return this.worker.getDebugData();
|
||||
}
|
||||
set useRuntimeCache(useCache: boolean) {
|
||||
this.worker.setUseRuntimeCache(useCache);
|
||||
}
|
||||
|
||||
@@ -59,34 +59,9 @@ export const AppSettingTypes = {
|
||||
label: 'Execute in WebWorker',
|
||||
value: true
|
||||
},
|
||||
showIndices: {
|
||||
advancedMode: {
|
||||
type: 'boolean',
|
||||
label: 'Show Indices',
|
||||
value: false
|
||||
},
|
||||
showPerformancePanel: {
|
||||
type: 'boolean',
|
||||
label: 'Show Performance Panel',
|
||||
value: false
|
||||
},
|
||||
showBenchmarkPanel: {
|
||||
type: 'boolean',
|
||||
label: 'Show Benchmark Panel',
|
||||
value: false
|
||||
},
|
||||
showVertices: {
|
||||
type: 'boolean',
|
||||
label: 'Show Vertices',
|
||||
value: false
|
||||
},
|
||||
showStemLines: {
|
||||
type: 'boolean',
|
||||
label: 'Show Stem Lines',
|
||||
value: false
|
||||
},
|
||||
showGraphJson: {
|
||||
type: 'boolean',
|
||||
label: 'Show Graph Source',
|
||||
label: 'Advanced Mode',
|
||||
value: false
|
||||
},
|
||||
cache: {
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
import { type Snippet } from 'svelte';
|
||||
import { panelState as state } from './PanelState.svelte';
|
||||
|
||||
const { children } = $props<{ children?: Snippet }>();
|
||||
let { children, open = $bindable(false) } = $props<{ children?: Snippet; open?: boolean }>();
|
||||
|
||||
$effect(() => {
|
||||
open = !!state.activePanel.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="wrapper" class:visible={state.activePanel.value}>
|
||||
|
||||
@@ -42,11 +42,13 @@
|
||||
const store: Store = {};
|
||||
Object.keys(inputs).forEach((key) => {
|
||||
if (props) {
|
||||
const value = props[key] || inputs[key].value;
|
||||
const value = props[key] !== undefined ? props[key] : inputs[key].value;
|
||||
if (Array.isArray(value) || typeof value === 'number') {
|
||||
store[key] = value;
|
||||
} else if (typeof value === 'boolean') {
|
||||
store[key] = value ? 1 : 0;
|
||||
} else {
|
||||
console.error('Wrong error');
|
||||
console.error('Wrong error', { value });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Grid from '$lib/grid';
|
||||
import { debounceAsyncFunction } from '$lib/helpers';
|
||||
import { createKeyMap } from '$lib/helpers/createKeyMap';
|
||||
import { debugNode } from '$lib/node-registry/debugNode.js';
|
||||
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
|
||||
import NodeStore from '$lib/node-store/NodeStore.svelte';
|
||||
import PerformanceViewer from '$lib/performance/PerformanceViewer.svelte';
|
||||
@@ -32,7 +33,8 @@
|
||||
const { data } = $props();
|
||||
|
||||
const registryCache = new IndexDBCache('node-registry');
|
||||
const nodeRegistry = new RemoteNodeRegistry('', registryCache);
|
||||
|
||||
const nodeRegistry = new RemoteNodeRegistry('', registryCache, [debugNode]);
|
||||
const workerRuntime = new WorkerRuntimeExecutor();
|
||||
const runtimeCache = new MemoryRuntimeCache();
|
||||
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
|
||||
@@ -63,8 +65,10 @@
|
||||
let activeNode = $state<NodeInstance | undefined>(undefined);
|
||||
let scene = $state<Group>(null!);
|
||||
|
||||
let sidebarOpen = $state(false);
|
||||
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
|
||||
let viewerComponent = $state<ReturnType<typeof Viewer>>();
|
||||
let debugData = $state<Record<number, { type: string; data: Int32Array }>>();
|
||||
const manager = $derived(graphInterface?.manager);
|
||||
|
||||
async function randomGenerate() {
|
||||
@@ -86,11 +90,6 @@
|
||||
let graphSettingTypes = $state({
|
||||
randomSeed: { type: 'boolean', value: false }
|
||||
});
|
||||
$effect(() => {
|
||||
if (graphSettings && graphSettingTypes) {
|
||||
manager?.setSettings($state.snapshot(graphSettings));
|
||||
}
|
||||
});
|
||||
|
||||
async function update(
|
||||
g: Graph,
|
||||
@@ -104,6 +103,7 @@
|
||||
|
||||
if (appSettings.value.debug.useWorker) {
|
||||
let perfData = await runtime.getPerformanceData();
|
||||
debugData = await runtime.getDebugData();
|
||||
let lastRun = perfData?.at(-1);
|
||||
if (lastRun?.total) {
|
||||
lastRun.runtime = lastRun.total;
|
||||
@@ -162,6 +162,7 @@
|
||||
bind:scene
|
||||
bind:this={viewerComponent}
|
||||
perf={performanceStore}
|
||||
debugData={debugData}
|
||||
centerCamera={appSettings.value.centerCamera}
|
||||
/>
|
||||
</Grid.Cell>
|
||||
@@ -171,6 +172,7 @@
|
||||
graph={pm.graph}
|
||||
bind:this={graphInterface}
|
||||
registry={nodeRegistry}
|
||||
addMenuPadding={{ right: sidebarOpen ? 330 : undefined }}
|
||||
backgroundType={appSettings.value.nodeInterface.backgroundType}
|
||||
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
|
||||
bind:activeNode
|
||||
@@ -181,7 +183,7 @@
|
||||
onresult={(result) => handleUpdate(result as Graph)}
|
||||
/>
|
||||
{/if}
|
||||
<Sidebar>
|
||||
<Sidebar bind:open={sidebarOpen}>
|
||||
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
||||
<NestedSettings
|
||||
id="general"
|
||||
@@ -214,7 +216,7 @@
|
||||
<Panel
|
||||
id="performance"
|
||||
title="Performance"
|
||||
hidden={!appSettings.value.debug.showPerformancePanel}
|
||||
hidden={!appSettings.value.debug.advancedMode}
|
||||
icon="i-[tabler--brand-speedtest] bg-red-400"
|
||||
>
|
||||
{#if $performanceStore}
|
||||
@@ -227,7 +229,7 @@
|
||||
<Panel
|
||||
id="graph-source"
|
||||
title="Graph Source"
|
||||
hidden={!appSettings.value.debug.showGraphJson}
|
||||
hidden={!appSettings.value.debug.advancedMode}
|
||||
icon="i-[tabler--code]"
|
||||
>
|
||||
<GraphSource graph={pm.graph ?? manager?.serialize()} />
|
||||
@@ -235,7 +237,7 @@
|
||||
<Panel
|
||||
id="benchmark"
|
||||
title="Benchmark"
|
||||
hidden={!appSettings.value.debug.showBenchmarkPanel}
|
||||
hidden={!appSettings.value.debug.advancedMode}
|
||||
icon="i-[tabler--graph] bg-red-400"
|
||||
>
|
||||
<BenchmarkPanel run={randomGenerate} />
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
const { children } = $props<{ children?: Snippet }>();
|
||||
</script>
|
||||
|
||||
<main class="w-screen overflow-x-hidden">
|
||||
<main class="w-screen h-screen overflow-x-hidden">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
@@ -44,8 +44,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
fetchNodeData(activeNode.value);
|
||||
let graphSettings = $state<Record<string, any>>({});
|
||||
let graphSettingTypes = $state({
|
||||
randomSeed: { type: "boolean", value: false },
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -61,19 +62,85 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="node-wrapper absolute bottom-8 left-8">
|
||||
{#if nodeInstance}
|
||||
<NodeHTML inView position="relative" z={5} bind:node={nodeInstance} />
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:window
|
||||
bind:innerHeight={windowHeight}
|
||||
onkeydown={(ev) => ev.key === "r" && handleResult()}
|
||||
/>
|
||||
|
||||
<Grid.Row>
|
||||
<Grid.Cell>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(nodeInstance?.props)}
|
||||
</code>
|
||||
</pre>
|
||||
{#if visibleRows?.length}
|
||||
<table
|
||||
class="min-w-full select-none overflow-auto text-left text-sm flex-1"
|
||||
onscroll={(e) => {
|
||||
const scrollTop = e.currentTarget.scrollTop;
|
||||
start.value = Math.floor(scrollTop / rowHeight);
|
||||
}}
|
||||
>
|
||||
<thead class="">
|
||||
<tr>
|
||||
<th class="px-4 py-2 border-b border-[var(--outline)]">i</th>
|
||||
<th
|
||||
class="px-4 py-2 border-b border-[var(--outline)] w-[50px]"
|
||||
style:width="50px">Ptrs</th
|
||||
>
|
||||
<th class="px-4 py-2 border-b border-[var(--outline)]">Value</th>
|
||||
<th class="px-4 py-2 border-b border-[var(--outline)]">Float</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
onscroll={(e) => {
|
||||
const scrollTop = e.currentTarget.scrollTop;
|
||||
start.value = Math.floor(scrollTop / rowHeight);
|
||||
}}
|
||||
>
|
||||
{#each visibleRows as r, i}
|
||||
{@const index = i + start.value}
|
||||
{@const ptr = ptrs[i]}
|
||||
<tr class="h-[40px] odd:bg-[var(--layer-1)]">
|
||||
<td class="px-4 border-b border-[var(--outline)] w-8">{index}</td>
|
||||
<td
|
||||
class="border-b border-[var(--outline)] overflow-hidden text-ellipsis pl-2
|
||||
{ptr?._title?.includes('->')
|
||||
? 'bg-red-500'
|
||||
: 'bg-blue-500'}"
|
||||
style="width: 100px; min-width: 100px; max-width: 100px;"
|
||||
>
|
||||
{ptr?._title}
|
||||
</td>
|
||||
<td
|
||||
class="px-4 border-b border-[var(--outline)] cursor-pointer text-blue-600 hover:text-blue-800"
|
||||
onclick={() =>
|
||||
(rowIsFloat.value[index] = !rowIsFloat.value[index])}
|
||||
>
|
||||
{decodeValue(r, rowIsFloat.value[index])}
|
||||
</td>
|
||||
<td class="px-4 border-b border-[var(--outline)] italic w-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rowIsFloat.value[index]}
|
||||
onclick={() =>
|
||||
(rowIsFloat.value[index] = !rowIsFloat.value[index])}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<button
|
||||
onclick={() => copyVisibleMemory(visibleRows, ptrs, start.value)}
|
||||
class="flex items-center cursor-pointer absolute bottom-4 left-4 z-100 bg-gray-200 px-2 py-1 rounded hover:bg-gray-300"
|
||||
>
|
||||
Copy Visible Memory
|
||||
</button>
|
||||
<input
|
||||
class="absolute bottom-4 right-4 bg-white"
|
||||
bind:value={start.value}
|
||||
min="0"
|
||||
type="number"
|
||||
step="1"
|
||||
/>
|
||||
{/if}
|
||||
</Grid.Cell>
|
||||
|
||||
<Grid.Cell>
|
||||
@@ -82,6 +149,20 @@
|
||||
</Grid.Row>
|
||||
|
||||
<Sidebar>
|
||||
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
||||
<h3 class="p-4 pb-0">Debug Settings</h3>
|
||||
<NestedSettings
|
||||
id="Debug"
|
||||
bind:value={devSettings.value}
|
||||
type={DevSettingsType}
|
||||
/>
|
||||
<hr />
|
||||
<NestedSettings
|
||||
id="general"
|
||||
bind:value={appSettings.value}
|
||||
type={AppSettingTypes}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel
|
||||
id="node-store"
|
||||
classes="text-green-400"
|
||||
|
||||
74
app/src/routes/dev/dev-graph.json
Normal file
74
app/src/routes/dev/dev-graph.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"settings": {
|
||||
"resolution.circle": 26,
|
||||
"resolution.curve": 39
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 9,
|
||||
"position": [
|
||||
225,
|
||||
65
|
||||
],
|
||||
"type": "max/plantarium/output",
|
||||
"props": {
|
||||
"out": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"position": [
|
||||
200,
|
||||
60
|
||||
],
|
||||
"type": "max/plantarium/math",
|
||||
"props": {
|
||||
"op_type": 3,
|
||||
"a": 2,
|
||||
"b": 0.38
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"position": [
|
||||
175,
|
||||
60
|
||||
],
|
||||
"type": "max/plantarium/float",
|
||||
"props": {
|
||||
"value": 0.66
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"position": [
|
||||
175,
|
||||
80
|
||||
],
|
||||
"type": "max/plantarium/float",
|
||||
"props": {
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
[
|
||||
11,
|
||||
0,
|
||||
10,
|
||||
"a"
|
||||
],
|
||||
[
|
||||
12,
|
||||
0,
|
||||
10,
|
||||
"b"
|
||||
],
|
||||
[
|
||||
10,
|
||||
0,
|
||||
9,
|
||||
"out"
|
||||
]
|
||||
]
|
||||
}
|
||||
48
app/src/routes/dev/helpers.ts
Normal file
48
app/src/routes/dev/helpers.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Pointer } from '$lib/runtime';
|
||||
|
||||
export function copyVisibleMemory(rows: Int32Array, currentPtrs: Pointer[], start: number) {
|
||||
if (!rows?.length) return;
|
||||
|
||||
// Build an array of rows for the table
|
||||
const tableRows = [...rows].map((value, i) => {
|
||||
const index = start + i;
|
||||
const ptr = currentPtrs[i];
|
||||
return {
|
||||
index,
|
||||
ptr: ptr?._title ?? '',
|
||||
value: value
|
||||
};
|
||||
});
|
||||
|
||||
// Compute column widths
|
||||
const indexWidth = Math.max(
|
||||
5,
|
||||
...tableRows.map((r) => r.index.toString().length)
|
||||
);
|
||||
const ptrWidth = Math.max(
|
||||
10,
|
||||
...tableRows.map((r) => r.ptr.length)
|
||||
);
|
||||
const valueWidth = Math.max(
|
||||
10,
|
||||
...tableRows.map((r) => r.value.toString().length)
|
||||
);
|
||||
|
||||
// Build header
|
||||
let output =
|
||||
`| ${'Index'.padEnd(indexWidth)} | ${'Ptr'.padEnd(ptrWidth)} | ${'Value'.padEnd(valueWidth)
|
||||
} |\n`
|
||||
+ `|-${'-'.repeat(indexWidth)}-|-${'-'.repeat(ptrWidth)}-|-${'-'.repeat(valueWidth)}-|\n`;
|
||||
|
||||
// Add rows
|
||||
for (const row of tableRows) {
|
||||
output += `| ${row.index.toString().padEnd(indexWidth)} | ${row.ptr.padEnd(ptrWidth)} | ${row.value.toString().padEnd(valueWidth)
|
||||
} |\n`;
|
||||
}
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(output)
|
||||
.then(() => console.log('Memory + metadata copied as table'))
|
||||
.catch((err) => console.error('Failed to copy memory:', err));
|
||||
}
|
||||
15
app/src/routes/dev/settings.svelte.ts
Normal file
15
app/src/routes/dev/settings.svelte.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
import { settingsToStore } from '$lib/settings/app-settings.svelte';
|
||||
|
||||
export const DevSettingsType = {
|
||||
debugNode: {
|
||||
type: 'boolean',
|
||||
label: 'Debug Nodes',
|
||||
value: true
|
||||
}
|
||||
} as const;
|
||||
|
||||
export let devSettings = localState(
|
||||
'dev-settings',
|
||||
settingsToStore(DevSettingsType)
|
||||
);
|
||||
19
app/static/favicon.svg
Normal file
19
app/static/favicon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M55.2154 77.6602L52.2788 78.3944L49.2607 60.6933L52.2788 33.0404L47.6293 18.0312L45.9162 18.6838L41.6745 28.7987L31.6412 33.4483H20.2211L21.6894 24.0675L31.6412 15.9919L45.9162 15.1762L49.7501 15.9919L55.2154 32.6326L54.5628 38.5873L64.8409 33.0404L69.5721 37.69L80.1764 43.1553L84.1734 52.8624L83.113 64.1193L73.8954 61.8353L66.3092 52.4545V38.5873L64.1068 36.7112L54.155 42.4212L52.2788 60.6933L55.2154 77.6602Z"
|
||||
fill="url(#paint0_linear)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear"
|
||||
x1="34.3903"
|
||||
y1="15.1762"
|
||||
x2="52.1972"
|
||||
y2="78.3944"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#4CAF7B" />
|
||||
<stop offset="1" stop-color="#347452" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
@@ -1,19 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--logos"
|
||||
width="26.6"
|
||||
height="32"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 256 308"
|
||||
>
|
||||
<path
|
||||
fill="#FF3E00"
|
||||
d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"
|
||||
></path><path
|
||||
fill="#FFF"
|
||||
d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"
|
||||
></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,26 +0,0 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z"
|
||||
fill="#FFC131"
|
||||
/>
|
||||
<ellipse
|
||||
cx="84.1426"
|
||||
cy="147"
|
||||
rx="22"
|
||||
ry="22"
|
||||
transform="rotate(180 84.1426 147)"
|
||||
fill="#24C8DB"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z"
|
||||
fill="#FFC131"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z"
|
||||
fill="#24C8DB"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,37 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--logos"
|
||||
width="31.88"
|
||||
height="32"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 256 257"
|
||||
>
|
||||
<defs><linearGradient
|
||||
id="IconifyId1813088fe1fbc01fb466"
|
||||
x1="-.828%"
|
||||
x2="57.636%"
|
||||
y1="7.652%"
|
||||
y2="78.411%"
|
||||
><stop offset="0%" stop-color="#41D1FF"></stop><stop
|
||||
offset="100%"
|
||||
stop-color="#BD34FE"
|
||||
></stop></linearGradient><linearGradient
|
||||
id="IconifyId1813088fe1fbc01fb467"
|
||||
x1="43.376%"
|
||||
x2="50.316%"
|
||||
y1="2.242%"
|
||||
y2="89.03%"
|
||||
><stop offset="0%" stop-color="#FFEA83"></stop><stop
|
||||
offset="8.333%"
|
||||
stop-color="#FFDD35"
|
||||
></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path
|
||||
fill="url(#IconifyId1813088fe1fbc01fb466)"
|
||||
d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"
|
||||
></path><path
|
||||
fill="url(#IconifyId1813088fe1fbc01fb467)"
|
||||
d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -4,20 +4,19 @@ This guide will help you developing your first Nodarium Node written in Rust. As
|
||||
|
||||
## Prerequesites
|
||||
|
||||
You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) installed. Rust is the language we are going to develop our node in and wasm-pack helps us compile our rust code into a webassembly file.
|
||||
You need to have [Rust](https://www.rust-lang.org/tools/install) installed. Rust is the language we are going to develop our node in and cargo compiles our rust code into webassembly.
|
||||
|
||||
```bash
|
||||
# install rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
# install wasm-pack
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
## Clone Template
|
||||
|
||||
```bash
|
||||
wasm-pack new my-new-node --template https://github.com/jim-fx/nodarium_template
|
||||
cd my-new-node
|
||||
# copy the template directory
|
||||
cp -r nodes/max/plantarium/.template nodes/max/plantarium/my-new-node
|
||||
cd nodes/max/plantarium/my-new-node
|
||||
```
|
||||
|
||||
## Setup Definition
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{
|
||||
encode_float, evaluate_float, geometry::calculate_normals,log,
|
||||
split_args, wrap_arg,
|
||||
encode_float, evaluate_float, geometry::calculate_normals, wrap_arg,
|
||||
read_i32_slice
|
||||
};
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
pub fn execute(size: (i32, i32)) -> Vec<i32> {
|
||||
|
||||
let args = split_args(input);
|
||||
let args = read_i32_slice(size);
|
||||
|
||||
log!("WASM(cube): input: {:?} -> {:?}", input, args);
|
||||
|
||||
let size = evaluate_float(args[0]);
|
||||
let size = evaluate_float(&args);
|
||||
|
||||
let p = encode_float(size);
|
||||
let n = encode_float(-size);
|
||||
@@ -77,8 +75,6 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
|
||||
let res = wrap_arg(&cube_geometry);
|
||||
|
||||
log!("WASM(box): output: {:?}", res);
|
||||
|
||||
res
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
use nodarium_utils::{
|
||||
concat_arg_vecs, evaluate_float, evaluate_int,
|
||||
geometry::{
|
||||
@@ -13,15 +14,25 @@ use std::f32::consts::PI;
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
let args = split_args(input);
|
||||
|
||||
let paths = split_args(args[0]);
|
||||
pub fn execute(
|
||||
path: (i32, i32),
|
||||
length: (i32, i32),
|
||||
thickness: (i32, i32),
|
||||
offset_single: (i32, i32),
|
||||
lowest_branch: (i32, i32),
|
||||
highest_branch: (i32, i32),
|
||||
depth: (i32, i32),
|
||||
amount: (i32, i32),
|
||||
resolution_curve: (i32, i32),
|
||||
rotation: (i32, i32),
|
||||
) -> Vec<i32> {
|
||||
let arg = read_i32_slice(path);
|
||||
let paths = split_args(arg.as_slice());
|
||||
|
||||
let mut output: Vec<Vec<i32>> = Vec::new();
|
||||
|
||||
let resolution = evaluate_int(args[8]).max(4) as usize;
|
||||
let depth = evaluate_int(args[6]);
|
||||
let resolution = evaluate_int(read_i32_slice(resolution_curve).as_slice()).max(4) as usize;
|
||||
let depth = evaluate_int(read_i32_slice(depth).as_slice());
|
||||
|
||||
let mut max_depth = 0;
|
||||
for path_data in paths.iter() {
|
||||
@@ -40,18 +51,18 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
|
||||
let path = wrap_path(path_data);
|
||||
|
||||
let branch_amount = evaluate_int(args[7]).max(1);
|
||||
let branch_amount = evaluate_int(read_i32_slice(amount).as_slice()).max(1);
|
||||
|
||||
let lowest_branch = evaluate_float(args[4]);
|
||||
let highest_branch = evaluate_float(args[5]);
|
||||
let lowest_branch = evaluate_float(read_i32_slice(lowest_branch).as_slice());
|
||||
let highest_branch = evaluate_float(read_i32_slice(highest_branch).as_slice());
|
||||
|
||||
for i in 0..branch_amount {
|
||||
let a = i as f32 / (branch_amount - 1).max(1) as f32;
|
||||
|
||||
let length = evaluate_float(args[1]);
|
||||
let thickness = evaluate_float(args[2]);
|
||||
let length = evaluate_float(read_i32_slice(length).as_slice());
|
||||
let thickness = evaluate_float(read_i32_slice(thickness).as_slice());
|
||||
let offset_single = if i % 2 == 0 {
|
||||
evaluate_float(args[3])
|
||||
evaluate_float(read_i32_slice(offset_single).as_slice())
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -65,7 +76,8 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
root_alpha + (offset_single - 0.5) * 6.0 / resolution as f32,
|
||||
);
|
||||
|
||||
let rotation_angle = (evaluate_float(args[9]) * PI / 180.0) * i as f32;
|
||||
let rotation_angle =
|
||||
(evaluate_float(read_i32_slice(rotation).as_slice()) * PI / 180.0) * i as f32;
|
||||
|
||||
// check if diration contains NaN
|
||||
if orthogonal[0].is_nan() || orthogonal[1].is_nan() || orthogonal[2].is_nan() {
|
||||
|
||||
6
nodes/max/plantarium/debug/.gitignore
vendored
Normal file
6
nodes/max/plantarium/debug/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
||||
12
nodes/max/plantarium/debug/Cargo.toml
Normal file
12
nodes/max/plantarium/debug/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "debug"
|
||||
version = "0.1.0"
|
||||
authors = ["Max Richter <jim-x@web.de>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||
22
nodes/max/plantarium/debug/src/input.json
Normal file
22
nodes/max/plantarium/debug/src/input.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "max/plantarium/debug",
|
||||
"outputs": [],
|
||||
"inputs": {
|
||||
"input": {
|
||||
"type": "float",
|
||||
"accepts": [
|
||||
"*"
|
||||
],
|
||||
"external": true
|
||||
},
|
||||
"type": {
|
||||
"type": "select",
|
||||
"options": [
|
||||
"float",
|
||||
"vec3",
|
||||
"geometry"
|
||||
],
|
||||
"internal": true
|
||||
}
|
||||
}
|
||||
}
|
||||
25
nodes/max/plantarium/debug/src/lib.rs
Normal file
25
nodes/max/plantarium/debug/src/lib.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::encode_float;
|
||||
use nodarium_utils::evaluate_float;
|
||||
use nodarium_utils::evaluate_vec3;
|
||||
use nodarium_utils::read_i32;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: (i32, i32), input_type: (i32, i32)) -> Vec<i32> {
|
||||
let inp = read_i32_slice(input);
|
||||
let t = read_i32(input_type.0);
|
||||
if t == 0 {
|
||||
let f = evaluate_float(inp.as_slice());
|
||||
return vec![encode_float(f)];
|
||||
}
|
||||
if t == 1 {
|
||||
let f = evaluate_vec3(inp.as_slice());
|
||||
return vec![encode_float(f[0]), encode_float(f[1]), encode_float(f[2])];
|
||||
}
|
||||
|
||||
return inp;
|
||||
}
|
||||
@@ -2,11 +2,14 @@
|
||||
name = "float"
|
||||
version = "0.1.0"
|
||||
authors = ["Max Richter <jim-x@web.de>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
|
||||
[dependencies]
|
||||
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::read_i32;
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(args: &[i32]) -> Vec<i32> {
|
||||
args.into()
|
||||
pub fn execute(a: (i32, i32)) -> Vec<i32> {
|
||||
vec![read_i32(a.0)]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use glam::Vec3;
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
use nodarium_utils::{
|
||||
concat_args, evaluate_float, evaluate_int,
|
||||
geometry::{wrap_path, wrap_path_mut},
|
||||
@@ -14,13 +15,17 @@ fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
|
||||
}
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
pub fn execute(
|
||||
plant: (i32, i32),
|
||||
strength: (i32, i32),
|
||||
curviness: (i32, i32),
|
||||
depth: (i32, i32),
|
||||
) -> Vec<i32> {
|
||||
reset_call_count();
|
||||
|
||||
let args = split_args(input);
|
||||
|
||||
let plants = split_args(args[0]);
|
||||
let depth = evaluate_int(args[3]);
|
||||
let arg = read_i32_slice(plant);
|
||||
let plants = split_args(arg.as_slice());
|
||||
let depth = evaluate_int(read_i32_slice(depth).as_slice());
|
||||
|
||||
let mut max_depth = 0;
|
||||
for path_data in plants.iter() {
|
||||
@@ -55,9 +60,9 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
|
||||
let length = direction.length();
|
||||
|
||||
let curviness = evaluate_float(args[2]);
|
||||
let strength =
|
||||
evaluate_float(args[1]) / curviness.max(0.0001) * evaluate_float(args[1]);
|
||||
let str = evaluate_float(read_i32_slice(strength).as_slice());
|
||||
let curviness = evaluate_float(read_i32_slice(curviness).as_slice());
|
||||
let strength = str / curviness.max(0.0001) * str;
|
||||
|
||||
log!(
|
||||
"length: {}, curviness: {}, strength: {}",
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
let args = split_args(input);
|
||||
let mut inputs = split_args(args[0]);
|
||||
|
||||
let mut geo_data = args[1].to_vec();
|
||||
let mut geo_data = read_i32_slice(geometry);
|
||||
let geo = wrap_geometry_data(&mut geo_data);
|
||||
|
||||
let mut transforms: Vec<Mat4> = Vec::new();
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{
|
||||
concat_args, split_args
|
||||
};
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(args: &[i32]) -> Vec<i32> {
|
||||
let args = split_args(args);
|
||||
concat_args(vec![&[0], args[0], args[1], args[2]])
|
||||
}
|
||||
use nodarium_utils::log;
|
||||
use nodarium_utils::{concat_arg_vecs, read_i32_slice};
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(op_type: (i32, i32), a: (i32, i32), b: (i32, i32)) -> Vec<i32> {
|
||||
log!("math.op {:?}", op_type);
|
||||
let op = read_i32_slice(op_type);
|
||||
let a_val = read_i32_slice(a);
|
||||
let b_val = read_i32_slice(b);
|
||||
concat_arg_vecs(vec![vec![0], op, a_val, b_val])
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "noise"
|
||||
version = "0.1.0"
|
||||
authors = ["Max Richter <jim-x@web.de>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
use nodarium_utils::{
|
||||
concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut,
|
||||
concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut, read_i32,
|
||||
reset_call_count, split_args,
|
||||
};
|
||||
use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex};
|
||||
@@ -13,23 +14,31 @@ fn lerp(a: f32, b: f32, t: f32) -> f32 {
|
||||
}
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
pub fn execute(
|
||||
plant: (i32, i32),
|
||||
scale: (i32, i32),
|
||||
strength: (i32, i32),
|
||||
fix_bottom: (i32, i32),
|
||||
seed: (i32, i32),
|
||||
directional_strength: (i32, i32),
|
||||
depth: (i32, i32),
|
||||
octaves: (i32, i32),
|
||||
) -> Vec<i32> {
|
||||
reset_call_count();
|
||||
|
||||
let args = split_args(input);
|
||||
let arg = read_i32_slice(plant);
|
||||
let plants = split_args(arg.as_slice());
|
||||
let scale = (evaluate_float(read_i32_slice(scale).as_slice()) * 0.1) as f64;
|
||||
let strength = evaluate_float(read_i32_slice(strength).as_slice());
|
||||
let fix_bottom = evaluate_float(read_i32_slice(fix_bottom).as_slice());
|
||||
|
||||
let plants = split_args(args[0]);
|
||||
let scale = (evaluate_float(args[1]) * 0.1) as f64;
|
||||
let strength = evaluate_float(args[2]);
|
||||
let fix_bottom = evaluate_float(args[3]);
|
||||
let seed = read_i32(seed.0);
|
||||
|
||||
let seed = args[4][0];
|
||||
let directional_strength = evaluate_vec3(read_i32_slice(directional_strength).as_slice());
|
||||
|
||||
let directional_strength = evaluate_vec3(args[5]);
|
||||
let depth = evaluate_int(read_i32_slice(depth).as_slice());
|
||||
|
||||
let depth = evaluate_int(args[6]);
|
||||
|
||||
let octaves = evaluate_int(args[7]);
|
||||
let octaves = evaluate_int(read_i32_slice(octaves).as_slice());
|
||||
|
||||
let noise_x: HybridMulti<OpenSimplex> =
|
||||
HybridMulti::new(seed as u32 + 1).set_octaves(octaves as usize);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"input": {
|
||||
"type": "path",
|
||||
"accepts": [
|
||||
"geometry"
|
||||
"*"
|
||||
],
|
||||
"external": true
|
||||
},
|
||||
@@ -1,44 +1,11 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{
|
||||
concat_args, evaluate_int,
|
||||
geometry::{extrude_path, wrap_path},
|
||||
log, split_args,
|
||||
};
|
||||
use nodarium_utils::read_i32_slice;
|
||||
|
||||
nodarium_definition_file!("src/inputs.json");
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
log!("WASM(output): input: {:?}", input);
|
||||
|
||||
let args = split_args(input);
|
||||
|
||||
log!("WASM(output) args: {:?}", args);
|
||||
|
||||
assert_eq!(args.len(), 2, "Expected 2 arguments, got {}", args.len());
|
||||
let inputs = split_args(args[0]);
|
||||
|
||||
let resolution = evaluate_int(args[1]) as usize;
|
||||
|
||||
log!("inputs: {}, resolution: {}", inputs.len(), resolution);
|
||||
|
||||
let mut output: Vec<Vec<i32>> = Vec::new();
|
||||
for arg in inputs {
|
||||
let arg_type = arg[2];
|
||||
log!("arg_type: {}, \n {:?}", arg_type, arg,);
|
||||
|
||||
if arg_type == 0 {
|
||||
// if this is path we need to extrude it
|
||||
output.push(arg.to_vec());
|
||||
let path_data = wrap_path(arg);
|
||||
let geometry = extrude_path(path_data, resolution);
|
||||
output.push(geometry);
|
||||
continue;
|
||||
}
|
||||
|
||||
output.push(arg.to_vec());
|
||||
}
|
||||
|
||||
concat_args(output.iter().map(|v| v.as_slice()).collect())
|
||||
pub fn execute(input: (i32, i32), _res: (i32, i32)) -> Vec<i32> {
|
||||
let inp = read_i32_slice(input);
|
||||
return inp;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{concat_args, split_args};
|
||||
use nodarium_utils::concat_arg_vecs;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
|
||||
nodarium_definition_file!("src/definition.json");
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(args: &[i32]) -> Vec<i32> {
|
||||
let args = split_args(args);
|
||||
concat_args(vec![&[1], args[0], args[1], args[2]])
|
||||
pub fn execute(min: (i32, i32), max: (i32, i32), seed: (i32, i32)) -> Vec<i32> {
|
||||
nodarium_utils::log!("random execute start");
|
||||
concat_arg_vecs(vec![
|
||||
vec![1],
|
||||
read_i32_slice(min),
|
||||
read_i32_slice(max),
|
||||
read_i32_slice(seed),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
use glam::{Mat4, Vec3};
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
use nodarium_utils::{
|
||||
concat_args, evaluate_float, evaluate_int, geometry::wrap_path_mut, log,
|
||||
split_args,
|
||||
concat_args, evaluate_float, evaluate_int, geometry::wrap_path_mut, log, split_args,
|
||||
};
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
pub fn execute(
|
||||
plant: (i32, i32),
|
||||
axis: (i32, i32),
|
||||
angle: (i32, i32),
|
||||
spread: (i32, i32),
|
||||
) -> Vec<i32> {
|
||||
log!("DEBUG args: {:?}", plant);
|
||||
|
||||
log!("DEBUG args: {:?}", input);
|
||||
|
||||
let args = split_args(input);
|
||||
|
||||
let plants = split_args(args[0]);
|
||||
let axis = evaluate_int(args[1]); // 0 =x, 1 = y, 2 = z
|
||||
let spread = evaluate_int(args[3]);
|
||||
let arg = read_i32_slice(plant);
|
||||
let plants = split_args(arg.as_slice());
|
||||
let axis = evaluate_int(read_i32_slice(axis).as_slice()); // 0 =x, 1 = y, 2 = z
|
||||
let spread = evaluate_int(read_i32_slice(spread).as_slice());
|
||||
|
||||
let output: Vec<Vec<i32>> = plants
|
||||
.iter()
|
||||
@@ -32,7 +35,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
|
||||
let path = wrap_path_mut(&mut path_data);
|
||||
|
||||
let angle = evaluate_float(args[2]);
|
||||
let angle = evaluate_float(read_i32_slice(angle).as_slice());
|
||||
|
||||
let origin = [path.points[0], path.points[1], path.points[2]];
|
||||
|
||||
|
||||
@@ -3,30 +3,29 @@ use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{
|
||||
evaluate_float, evaluate_int, evaluate_vec3,
|
||||
geometry::{create_multiple_paths, wrap_multiple_paths},
|
||||
log, reset_call_count, split_args,
|
||||
log, reset_call_count,
|
||||
read_i32_slice, read_i32,
|
||||
};
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
pub fn execute(origin: (i32, i32), _amount: (i32,i32), length: (i32, i32), thickness: (i32, i32), resolution_curve: (i32, i32)) -> Vec<i32> {
|
||||
reset_call_count();
|
||||
|
||||
let args = split_args(input);
|
||||
let amount = evaluate_int(read_i32_slice(_amount).as_slice()) as usize;
|
||||
let path_resolution = read_i32(resolution_curve.0) as usize;
|
||||
|
||||
let amount = evaluate_int(args[1]) as usize;
|
||||
let path_resolution = evaluate_int(args[4]) as usize;
|
||||
|
||||
log!("stem args: {:?}", args);
|
||||
log!("stem args: amount={:?}", amount);
|
||||
|
||||
let mut stem_data = create_multiple_paths(amount, path_resolution, 1);
|
||||
|
||||
let mut stems = wrap_multiple_paths(&mut stem_data);
|
||||
|
||||
for stem in stems.iter_mut() {
|
||||
let origin = evaluate_vec3(args[0]);
|
||||
let length = evaluate_float(args[2]);
|
||||
let thickness = evaluate_float(args[3]);
|
||||
let origin = evaluate_vec3(read_i32_slice(origin).as_slice());
|
||||
let length = evaluate_float(read_i32_slice(length).as_slice());
|
||||
let thickness = evaluate_float(read_i32_slice(thickness).as_slice());
|
||||
let amount_points = stem.points.len() / 4;
|
||||
|
||||
for i in 0..amount_points {
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{
|
||||
decode_float, encode_float, evaluate_int, split_args, wrap_arg, log
|
||||
};
|
||||
use nodarium_utils::read_i32_slice;
|
||||
use nodarium_utils::{decode_float, encode_float, evaluate_int, log, wrap_arg};
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
|
||||
let args = split_args(input);
|
||||
|
||||
let size = evaluate_int(args[0]);
|
||||
pub fn execute(size: (i32, i32)) -> Vec<i32> {
|
||||
let size = evaluate_int(read_i32_slice(size).as_slice());
|
||||
let decoded = decode_float(size);
|
||||
let negative_size = encode_float(-decoded);
|
||||
|
||||
log!("WASM(triangle): input: {:?} -> {}", args[0],decoded);
|
||||
log!("WASM(triangle): input: {:?} -> {}", size, decoded);
|
||||
|
||||
// [[1,3, x, y, z, x, y,z,x,y,z]];
|
||||
wrap_arg(&[
|
||||
1, // 1: geometry
|
||||
3, // 3 vertices
|
||||
1, // 1 face
|
||||
1, // 1: geometry
|
||||
3, // 3 vertices
|
||||
1, // 1 face
|
||||
// this are the indeces for the face
|
||||
0, 2, 1,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
//
|
||||
negative_size, // x -> point 1
|
||||
0, // y
|
||||
0, // z
|
||||
negative_size, // x -> point 1
|
||||
0, // y
|
||||
0, // z
|
||||
//
|
||||
size, // x -> point 2
|
||||
0, // y
|
||||
0, // z
|
||||
size, // x -> point 2
|
||||
0, // y
|
||||
0, // z
|
||||
//
|
||||
0, // x -> point 3
|
||||
0, // y
|
||||
size, // z
|
||||
0, // x -> point 3
|
||||
0, // y
|
||||
size, // z
|
||||
// this is the normal for the single face 1065353216 == 1.0f encoded is i32
|
||||
0, 1065353216, 0,
|
||||
0, 1065353216, 0,
|
||||
0, 1065353216, 0,
|
||||
0,
|
||||
1065353216,
|
||||
0,
|
||||
0,
|
||||
1065353216,
|
||||
0,
|
||||
0,
|
||||
1065353216,
|
||||
0,
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use nodarium_macros::nodarium_definition_file;
|
||||
use nodarium_macros::nodarium_execute;
|
||||
use nodarium_utils::{concat_args, log, split_args};
|
||||
use nodarium_utils::concat_args;
|
||||
use nodarium_utils::log;
|
||||
use nodarium_utils::read_i32_slice;
|
||||
|
||||
nodarium_definition_file!("src/input.json");
|
||||
|
||||
#[nodarium_execute]
|
||||
pub fn execute(input: &[i32]) -> Vec<i32> {
|
||||
let args = split_args(input);
|
||||
log!("vec3 input: {:?}", input);
|
||||
log!("vec3 args: {:?}", args);
|
||||
concat_args(args)
|
||||
pub fn execute(x: (i32, i32), y: (i32, i32), z: (i32, i32)) -> Vec<i32> {
|
||||
log!("vec3 x: {:?}", x);
|
||||
concat_args(vec![
|
||||
read_i32_slice(x).as_slice(),
|
||||
read_i32_slice(y).as_slice(),
|
||||
read_i32_slice(z).as_slice(),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"scripts": {
|
||||
"postinstall": "pnpm run -r --filter 'ui' build",
|
||||
"lint": "pnpm run -r --parallel lint",
|
||||
"qa": "pnpm lint && pnpm check && pnpm test",
|
||||
"format": "pnpm dprint fmt",
|
||||
"format:check": "pnpm dprint check",
|
||||
"test": "pnpm run -r --parallel test",
|
||||
|
||||
@@ -6,96 +6,202 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use syn::parse_macro_input;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
fn add_line_numbers(input: String) -> String {
|
||||
return input
|
||||
input
|
||||
.split('\n')
|
||||
.enumerate()
|
||||
.map(|(i, line)| format!("{:2}: {}", i + 1, line))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn read_node_definition(file_path: &Path) -> NodeDefinition {
|
||||
let project_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let full_path = Path::new(&project_dir).join(file_path);
|
||||
let json_content = fs::read_to_string(&full_path).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Failed to read JSON file at '{}/{}': {}",
|
||||
project_dir,
|
||||
file_path.to_string_lossy(),
|
||||
err
|
||||
)
|
||||
});
|
||||
serde_json::from_str(&json_content).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"JSON file contains invalid JSON: \n{} \n{}",
|
||||
err,
|
||||
add_line_numbers(json_content.clone())
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn nodarium_execute(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input_fn = parse_macro_input!(item as syn::ItemFn);
|
||||
let _fn_name = &input_fn.sig.ident;
|
||||
let _fn_vis = &input_fn.vis;
|
||||
let fn_name = &input_fn.sig.ident;
|
||||
let fn_vis = &input_fn.vis;
|
||||
let fn_body = &input_fn.block;
|
||||
let inner_fn_name = syn::Ident::new(&format!("__nodarium_inner_{}", fn_name), fn_name.span());
|
||||
|
||||
let first_arg_ident = if let Some(syn::FnArg::Typed(pat_type)) = input_fn.sig.inputs.first() {
|
||||
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
|
||||
&pat_ident.ident
|
||||
} else {
|
||||
panic!("Expected a simple identifier for the first argument");
|
||||
}
|
||||
} else {
|
||||
panic!("The execute function must have at least one argument (the input slice)");
|
||||
};
|
||||
let def: NodeDefinition = read_node_definition(Path::new("src/input.json"));
|
||||
|
||||
let input_count = def.inputs.as_ref().map(|i| i.len()).unwrap_or(0);
|
||||
|
||||
validate_signature(&input_fn.sig, input_count, &def);
|
||||
|
||||
let input_param_names: Vec<_> = input_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg| {
|
||||
if let syn::FnArg::Typed(pat_type) = arg {
|
||||
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
|
||||
Some(pat_ident.ident.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let param_count = input_fn.sig.inputs.len();
|
||||
let total_c_params = param_count * 2;
|
||||
|
||||
let arg_names: Vec<_> = (0..total_c_params)
|
||||
.map(|i| syn::Ident::new(&format!("arg{i}"), input_fn.sig.span()))
|
||||
.collect();
|
||||
|
||||
let mut tuple_args = Vec::new();
|
||||
for i in 0..param_count {
|
||||
let start_name = &arg_names[i * 2];
|
||||
let end_name = &arg_names[i * 2 + 1];
|
||||
let tuple_arg = quote! {
|
||||
(#start_name, #end_name)
|
||||
};
|
||||
tuple_args.push(tuple_arg);
|
||||
}
|
||||
|
||||
// We create a wrapper that handles the C ABI and pointer math
|
||||
let expanded = quote! {
|
||||
|
||||
extern "C" {
|
||||
fn host_log_panic(ptr: *const u8, len: usize);
|
||||
fn host_log(ptr: *const u8, len: usize);
|
||||
fn __nodarium_log(ptr: *const u8, len: usize);
|
||||
}
|
||||
|
||||
fn setup_panic_hook() {
|
||||
static SET_HOOK: std::sync::Once = std::sync::Once::new();
|
||||
SET_HOOK.call_once(|| {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let msg = info.to_string();
|
||||
unsafe { host_log_panic(msg.as_ptr(), msg.len()); }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn __alloc(len: usize) -> *mut i32 {
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
let ptr = buf.as_mut_ptr();
|
||||
std::mem::forget(buf);
|
||||
ptr
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn init_panic_hook() {
|
||||
std::panic::set_hook(Box::new(|_info| {
|
||||
unsafe {
|
||||
__nodarium_log(b"PANIC\0".as_ptr(), 5);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn __free(ptr: *mut i32, len: usize) {
|
||||
unsafe {
|
||||
let _ = Vec::from_raw_parts(ptr, 0, len);
|
||||
}
|
||||
pub extern "C" fn init_allocator() {
|
||||
nodarium_utils::allocator::ALLOCATOR.init();
|
||||
}
|
||||
|
||||
static mut OUTPUT_BUFFER: Vec<i32> = Vec::new();
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn execute(ptr: *const i32, len: usize) -> *mut i32 {
|
||||
setup_panic_hook();
|
||||
// 1. Convert raw pointer to slice
|
||||
let input = unsafe { core::slice::from_raw_parts(ptr, len) };
|
||||
|
||||
// 2. Call the logic (which we define below)
|
||||
let result_data: Vec<i32> = internal_logic(input);
|
||||
|
||||
// 3. Use the static buffer for the result
|
||||
let result_len = result_data.len();
|
||||
unsafe {
|
||||
OUTPUT_BUFFER.clear();
|
||||
OUTPUT_BUFFER.reserve(result_len + 1);
|
||||
OUTPUT_BUFFER.push(result_len as i32);
|
||||
OUTPUT_BUFFER.extend(result_data);
|
||||
|
||||
OUTPUT_BUFFER.as_mut_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_logic(#first_arg_ident: &[i32]) -> Vec<i32> {
|
||||
#fn_vis fn #inner_fn_name(#( #input_param_names: (i32, i32) ),*) -> Vec<i32> {
|
||||
#fn_body
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#fn_vis extern "C" fn execute(output_pos: i32, #( #arg_names: i32 ),*) -> i32 {
|
||||
|
||||
nodarium_utils::allocator::ALLOCATOR.init();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_panic_hook();
|
||||
nodarium_utils::log!("before_fn");
|
||||
let result = #inner_fn_name(
|
||||
#( #tuple_args ),*
|
||||
);
|
||||
nodarium_utils::log!("after_fn: result_len={}", result.len());
|
||||
|
||||
let len_bytes = result.len() * 4;
|
||||
unsafe {
|
||||
let src = result.as_ptr() as *const u8;
|
||||
let dst = output_pos as *mut u8;
|
||||
nodarium_utils::log!("writing output_pos={:?} src={:?} len_bytes={:?}", output_pos, src, len_bytes);
|
||||
dst.copy_from_nonoverlapping(src, len_bytes);
|
||||
}
|
||||
|
||||
len_bytes as i32
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
fn validate_signature(fn_sig: &syn::Signature, expected_inputs: usize, def: &NodeDefinition) {
|
||||
let param_count = fn_sig.inputs.len();
|
||||
let expected_params = expected_inputs;
|
||||
|
||||
if param_count != expected_params {
|
||||
panic!(
|
||||
"Execute function has {} parameters but definition has {} inputs\n\
|
||||
Definition inputs: {:?}\n\
|
||||
Expected signature:\n\
|
||||
pub fn execute({}) -> Vec<i32>",
|
||||
param_count,
|
||||
expected_inputs,
|
||||
def.inputs
|
||||
.as_ref()
|
||||
.map(|i| i.keys().collect::<Vec<_>>())
|
||||
.unwrap_or_default(),
|
||||
(0..expected_inputs)
|
||||
.map(|i| format!("arg{i}: (i32, i32)"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
for (i, arg) in fn_sig.inputs.iter().enumerate() {
|
||||
match arg {
|
||||
syn::FnArg::Typed(pat_type) => {
|
||||
let type_str = quote! { #pat_type.ty }.to_string();
|
||||
let clean_type = type_str
|
||||
.trim()
|
||||
.trim_start_matches("_")
|
||||
.trim_end_matches(".ty")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !clean_type.contains("(") && !clean_type.contains(",") {
|
||||
panic!(
|
||||
"Parameter {i} has type '{clean_type}' but should be a tuple (i32, i32) representing (start, end) positions in memory",
|
||||
);
|
||||
}
|
||||
}
|
||||
syn::FnArg::Receiver(_) => {
|
||||
panic!("Execute function cannot have 'self' parameter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &fn_sig.output {
|
||||
syn::ReturnType::Type(_, ty) => {
|
||||
let is_vec = match &**ty {
|
||||
syn::Type::Path(tp) => tp
|
||||
.path
|
||||
.segments
|
||||
.first()
|
||||
.map(|seg| seg.ident == "Vec")
|
||||
.unwrap_or(false),
|
||||
_ => false,
|
||||
};
|
||||
if !is_vec {
|
||||
panic!("Execute function must return Vec<i32>");
|
||||
}
|
||||
}
|
||||
syn::ReturnType::Default => {
|
||||
panic!("Execute function must return Vec<i32>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn nodarium_definition_file(input: TokenStream) -> TokenStream {
|
||||
let path_lit = syn::parse_macro_input!(input as syn::LitStr);
|
||||
@@ -105,30 +211,23 @@ pub fn nodarium_definition_file(input: TokenStream) -> TokenStream {
|
||||
let full_path = Path::new(&project_dir).join(&file_path);
|
||||
|
||||
let json_content = fs::read_to_string(&full_path).unwrap_or_else(|err| {
|
||||
panic!("Failed to read JSON file at '{}/{}': {}", project_dir, file_path, err)
|
||||
panic!("Failed to read JSON file at '{project_dir}/{file_path}': {err}",)
|
||||
});
|
||||
|
||||
let _: NodeDefinition = serde_json::from_str(&json_content).unwrap_or_else(|err| {
|
||||
panic!("JSON file contains invalid JSON: \n{} \n{}", err, add_line_numbers(json_content.clone()))
|
||||
panic!(
|
||||
"JSON file contains invalid JSON: \n{} \n{}",
|
||||
err,
|
||||
add_line_numbers(json_content.clone())
|
||||
)
|
||||
});
|
||||
|
||||
// We use the span from the input path literal
|
||||
let bytes = syn::LitByteStr::new(json_content.as_bytes(), path_lit.span());
|
||||
let len = json_content.len();
|
||||
|
||||
let expanded = quote! {
|
||||
#[link_section = "nodarium_definition"]
|
||||
static DEFINITION_DATA: [u8; #len] = *#bytes;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_definition_ptr() -> *const u8 {
|
||||
DEFINITION_DATA.as_ptr()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_definition_len() -> usize {
|
||||
DEFINITION_DATA.len()
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodarium/types",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,6 +9,7 @@ const DefaultOptionsSchema = z.object({
|
||||
accepts: z
|
||||
.array(
|
||||
z.union([
|
||||
z.literal('*'),
|
||||
z.literal('float'),
|
||||
z.literal('integer'),
|
||||
z.literal('boolean'),
|
||||
@@ -89,6 +90,12 @@ export const NodeInputPathSchema = z.object({
|
||||
value: z.array(z.number()).optional()
|
||||
});
|
||||
|
||||
export const NodeInputAnySchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal('*'),
|
||||
value: z.any().optional()
|
||||
});
|
||||
|
||||
export const NodeInputSchema = z.union([
|
||||
NodeInputSeedSchema,
|
||||
NodeInputBooleanSchema,
|
||||
@@ -100,7 +107,8 @@ export const NodeInputSchema = z.union([
|
||||
NodeInputSeedSchema,
|
||||
NodeInputVec3Schema,
|
||||
NodeInputGeometrySchema,
|
||||
NodeInputPathSchema
|
||||
NodeInputPathSchema,
|
||||
NodeInputAnySchema
|
||||
]);
|
||||
|
||||
export type NodeInput = z.infer<typeof NodeInputSchema>;
|
||||
|
||||
@@ -21,7 +21,7 @@ export type NodeRuntimeState = {
|
||||
parents?: NodeInstance[];
|
||||
children?: NodeInstance[];
|
||||
inputNodes?: Record<string, NodeInstance>;
|
||||
type?: NodeDefinition;
|
||||
type?: NodeDefinition; // we should probably remove this and rely on registry.getNode(nodeType)
|
||||
downX?: number;
|
||||
downY?: number;
|
||||
x?: number;
|
||||
@@ -65,7 +65,7 @@ export const NodeSchema = z.object({
|
||||
export type SerializedNode = z.infer<typeof NodeSchema>;
|
||||
|
||||
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
|
||||
execute(input: Int32Array): Int32Array;
|
||||
execute(outputPos: number, args: number[]): number;
|
||||
};
|
||||
|
||||
export type Socket = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodarium/ui",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && npm run package",
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
prefix: "i";
|
||||
}
|
||||
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-0");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-1");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-2");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-3");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-0{/30,/50,}");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-1{/30,/50,}");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-2{/30,/50,}");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-3{/30,/50,}");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}active");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}selected");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}outline{!,}");
|
||||
@@ -80,6 +80,7 @@ html {
|
||||
--neutral-100: #e7e7e7;
|
||||
--neutral-200: #cecece;
|
||||
--neutral-300: #7c7c7c;
|
||||
--neutral-350: #808080;
|
||||
--neutral-400: #2d2d2d;
|
||||
--neutral-500: #171717;
|
||||
--neutral-800: #111111;
|
||||
@@ -107,7 +108,7 @@ body {
|
||||
|
||||
html.theme-light {
|
||||
--color-text: var(--neutral-800);
|
||||
--color-outline: var(--neutral-300);
|
||||
--color-outline: var(--neutral-350);
|
||||
--color-layer-0: var(--neutral-050);
|
||||
--color-layer-1: var(--neutral-100);
|
||||
--color-layer-2: var(--neutral-200);
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<button
|
||||
aria-label="step down"
|
||||
onmousedown={stepDown}
|
||||
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
|
||||
class="cursor-pointer w-4 bg-layer-3/30 hover:bg-layer-3/50"
|
||||
>
|
||||
<span class="i-[tabler--chevron-compact-left] block h-full w-full text-outline!"></span>
|
||||
</button>
|
||||
@@ -161,7 +161,7 @@
|
||||
<button
|
||||
aria-label="step up"
|
||||
onmousedown={stepUp}
|
||||
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
|
||||
class="cursor-pointer w-4 bg-layer-3/30 hover:bg-layer-3/50"
|
||||
>
|
||||
<span class="i-[tabler--chevron-compact-right] block h-full w-full text-outline!"></span>
|
||||
</button>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<InputCheckbox bind:value={mirrorShape} />
|
||||
<p>mirror</p>
|
||||
</label>
|
||||
<p>{JSON.stringify(points)}</p>
|
||||
<p class="max-w-full overflow-hidden">{JSON.stringify(points)}</p>
|
||||
{/snippet}
|
||||
<div style:width="300px">
|
||||
<InputShape bind:value={points} mirror={mirrorShape} />
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<section class="border-outline border-1/2 bg-layer-1 rounded border mb-4 p-4 flex flex-col gap-4 {_class}">
|
||||
<h3 class="flex gap-2 font-bold">
|
||||
{title}
|
||||
<div class="flex gap-4 w-full font-normal opacity-50 max-w-[75%] whitespace-pre overflow-hidden text-clip">
|
||||
<div class="flex gap-4 w-full font-normal opacity-50 max-w-[75%]">
|
||||
{#if header}
|
||||
{@render header()}
|
||||
{:else}
|
||||
|
||||
@@ -9,6 +9,10 @@ description = "A collection of utilities for Nodarium"
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
||||
|
||||
[dependencies]
|
||||
glam = "0.30.10"
|
||||
noise = "0.9.0"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@nodarium/utils",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"format": "dprint fmt -c '../../.dprint.jsonc' .",
|
||||
|
||||
68
packages/utils/src/allocator.rs
Normal file
68
packages/utils/src/allocator.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use core::alloc::{GlobalAlloc, Layout};
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
extern "C" {
|
||||
fn __wasm_memory_size() -> usize;
|
||||
fn __nodarium_manual_end() -> usize;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const WASM_PAGE_SIZE: usize = 64 * 1024;
|
||||
|
||||
pub struct UpwardBumpAllocator {
|
||||
heap_base: AtomicUsize,
|
||||
}
|
||||
|
||||
impl Default for UpwardBumpAllocator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpwardBumpAllocator {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
heap_base: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn init(&self) {
|
||||
// Start heap at 10000 to leave space for data sections
|
||||
self.heap_base.store(10000, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
pub static ALLOCATOR: UpwardBumpAllocator = UpwardBumpAllocator::new();
|
||||
|
||||
unsafe impl GlobalAlloc for UpwardBumpAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
let align = layout.align();
|
||||
let size = layout.size();
|
||||
|
||||
let mut current = self.heap_base.load(Ordering::Relaxed);
|
||||
|
||||
loop {
|
||||
let aligned = (current + align - 1) & !(align - 1);
|
||||
let new_current = aligned + size;
|
||||
|
||||
let manual_end = unsafe { __nodarium_manual_end() };
|
||||
if new_current > manual_end {
|
||||
return core::ptr::null_mut();
|
||||
}
|
||||
|
||||
match self.heap_base.compare_exchange(
|
||||
current,
|
||||
new_current,
|
||||
Ordering::SeqCst,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => return aligned as *mut u8,
|
||||
Err(next) => current = next,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user