157 Commits

Author SHA1 Message Date
f16ba2601f fix(ci): still trying to get gpg to work
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m54s
2026-02-13 02:43:02 +01:00
cc6b832f15 fix(ci): trying to get gpg to work
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 3m22s
2026-02-13 02:25:11 +01:00
dd5fd5bf17 fix(ci): better add updates to package.json
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 4m0s
2026-02-13 02:10:34 +01:00
38d0fffcf4 chore: update ci image
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 3m48s
2026-02-13 01:58:16 +01:00
bce06da456 ci: add gpg-agent to ci image
Some checks failed
Build & Push CI Image / build-and-push (push) Successful in 8m43s
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-13 01:47:32 +01:00
af585d56ec feat: use new ci image with gpg
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 3m45s
2026-02-13 01:24:19 +01:00
0aa73a27c1 feat: install gpg in ci image
Some checks failed
Build & Push CI Image / build-and-push (push) Successful in 10m7s
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-13 01:13:01 +01:00
c1ae70282c feat: add color to sockets
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 3m5s
Closes #34
2026-02-13 00:57:28 +01:00
4c7b03dfb8 feat: add gradient mesh line 2026-02-13 00:51:21 +01:00
144e8cc797 fix: correctly highlight possible outputs 2026-02-12 23:38:44 +01:00
12ff9c1518 Merge pull request 'feat/debug-node' (#41) from feat/debug-node into main
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m58s
Reviewed-on: #41
2026-02-12 23:20:58 +01:00
8d3ffe84ab Merge branch 'main' into feat/debug-node
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m54s
2026-02-12 23:05:09 +01:00
95ec93eead feat: better handle ctrl+shift clicks and selections
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m45s
2026-02-12 22:46:50 +01:00
d39185efaf feat: add "pnpm qa" command to check before commit
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m52s
2026-02-12 22:33:37 +01:00
81580ccd8c fix: cleanup some type errors 2026-02-12 22:33:25 +01:00
bf6f632d27 feat: add shortcut to quick connect to debug
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 1m22s
2026-02-12 22:27:11 +01:00
release-bot
e098be6013 fix: also execute all nodes before debug node
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m56s
2026-02-12 21:57:33 +01:00
release-bot
ec13850e1c fix: make debug node work with runtime 2026-02-12 21:42:44 +01:00
release-bot
15e08a8163 feat: implement debug node
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m53s
Closes #39
2026-02-12 21:33:47 +01:00
release-bot
48cee58ad3 chore: update test snapshots
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m8s
2026-02-12 18:26:13 +01:00
release-bot
3235cae904 chore: fix lint and typecheck errors
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 3m15s
2026-02-12 18:19:27 +01:00
release-bot
3f440728fc feat: implement variable height for node shader
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 1m3s
2026-02-12 18:11:14 +01:00
release-bot
da09f8ba1e refactor: move debug node into runtime 2026-02-12 16:18:29 +01:00
release-bot
ddc3b4ce35 feat: allow variable height node parameters 2026-02-12 16:18:12 +01:00
release-bot
2690fc8712 chore: gitignore pnpm-store
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m6s
2026-02-12 15:42:38 +01:00
release-bot
072ab9063b feat: add initial debug node 2026-02-12 14:00:18 +01:00
release-bot
e23cad254d feat: add "*" datatype for inputs for debug node 2026-02-12 14:00:06 +01:00
release-bot
5b5c63c1a9 fix(ui): make arrows on inputnumber visible on lighttheme 2026-02-12 13:31:34 +01:00
release-bot
c9021f2383 refactor: merge all dev settings into one setting 2026-02-12 13:10:14 +01:00
9eecdd4fb8 Merge pull request 'feat: merge localState recursively with initial' (#38) from feat/debug-node into main
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m50s
Reviewed-on: #38
2026-02-12 12:51:28 +01:00
release-bot
7e71a41e52 feat: merge localState recursively with initial
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m0s
Closes #17
2026-02-12 12:50:58 +01:00
release-bot
07cd9e84eb feat: clamp AddMenu to viewport
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m10s
2026-02-10 21:51:50 +01:00
release-bot
a31a49ad50 ci: lint and typecheck before build
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m42s
2026-02-10 15:54:50 +01:00
release-bot
850d641a25 chore: pnpm format
Some checks failed
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-10 15:54:01 +01:00
release-bot
ee5ca81757 ci: sign release commits with pgp key
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 2m10s
2026-02-10 15:47:42 +01:00
release-bot
22a11832b8 fix(ci): correctly format changelog 2026-02-10 15:24:23 +01:00
release-bot
b5ce5723fa chore: format favicon svg
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m53s
2026-02-10 15:14:35 +01:00
release-bot
102130cc77 feat: add favicon
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 2m5s
2026-02-10 15:11:58 +01:00
release-bot
1668a2e6d5 chore: format changelog.md
Some checks failed
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-10 15:09:03 +01:00
release-bot
b0af83004e chore(release): v0.0.4 2026-02-10 14:03:49 +00:00
release-bot
51de3ced13 fix(ci): update changelog before building
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m47s
2026-02-10 14:59:17 +01:00
8d403ba803 Merge pull request 'feat/shape-node' (#36) from feat/shape-node into main
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m0s
Reviewed-on: #36
2026-02-09 22:32:14 +01:00
release-bot
6bb301153a Merge remote-tracking branch 'origin/main' into feat/shape-node
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m47s
2026-02-09 22:27:43 +01:00
release-bot
02eee5f9bf fix: disable macro logs in wasm
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m47s
2026-02-09 22:21:28 +01:00
release-bot
4f48a519a9 feat(nodes): add rotation to instance node
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m9s
2026-02-09 22:16:20 +01:00
release-bot
97199ac20f feat(nodes): implement leaf node 2026-02-09 22:16:02 +01:00
release-bot
f36f0cb230 feat(ui): show circles only when hovering InputShape 2026-02-09 22:15:39 +01:00
release-bot
ed3d48e07f fix(runtime): correctly encode 2d shape for wasm nodes 2026-02-09 22:15:11 +01:00
release-bot
c610d6c991 fix(app): show backside in three instances 2026-02-09 22:14:45 +01:00
8865b9b032 feat(node): initial leaf / shape nodes
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m1s
2026-02-09 18:32:52 +01:00
235ee5d979 fix(app): wrong linter errors in changelog
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m37s
2026-02-09 16:54:45 +01:00
23a48572f3 feat(app): dots background for node interface 2026-02-09 16:53:57 +01:00
e89a46e146 feat(app): add error page
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m20s
2026-02-09 16:18:27 +01:00
cefda41fcf feat(theme): optimize node readability 2026-02-09 16:18:19 +01:00
21d0f0da5a feat: add high-contrast-light theme 2026-02-09 16:04:17 +01:00
46202451ba ci: simplify ci quality checks
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m43s
2026-02-09 15:51:55 +01:00
0f4239d179 ci: simplify ci quality checks
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 38s
2026-02-09 15:50:05 +01:00
d9c9bb5234 fix(theme): allow raw html in head style 2026-02-09 15:49:50 +01:00
18802fdc10 fix(ui): add missing types
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 2m45s
2026-02-09 15:37:37 +01:00
b1cbd23542 feat(app): use same color for node outline and header
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 2m3s
2026-02-09 15:30:40 +01:00
33f10da396 feat(ui): make details stand out
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 2m6s
2026-02-09 15:26:48 +01:00
af5b3b23ba fix: make sure that CHANGELOG.md is in correct place 2026-02-09 15:26:40 +01:00
64d75b9686 feat(ui): add InputColor and custom theme 2026-02-09 15:26:18 +01:00
release-bot
2e6466ceca chore: update dprint linters
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m17s
2026-02-09 01:58:05 +01:00
release-bot
20d8e2abed feat(theme): improve light theme a bit 2026-02-09 01:57:32 +01:00
release-bot
715e1d095b feat(theme): merge edge and connection color
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m35s
2026-02-09 01:35:41 +01:00
release-bot
07e2826f16 feat(ui): improve colors of input shape 2026-02-09 00:52:35 +01:00
release-bot
e0ad97b003 feat(ui): highlight circle on hover on InputShape
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m53s
2026-02-09 00:21:58 +01:00
release-bot
93df4a19ff fix(ci): handle newline in commit messages for git.json
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 3m41s
2026-02-09 00:09:28 +01:00
release-bot
d661a4e4a9 feat(ui): improve InputShape ux
All checks were successful
🚀 Lint & Test & Deploy / release (pull_request) Successful in 4m17s
Allow interactions in mirrored side aswell. Use rightclick to delete
circles.
2026-02-08 23:59:39 +01:00
release-bot
c7f808ce2d wip 2026-02-08 22:56:41 +01:00
release-bot
72d6cd6ea2 feat(ui): add initial InputShape element 2026-02-08 21:59:43 +01:00
release-bot
615f2d3c48 feat(ui): allow custom snippets in ui section header 2026-02-08 21:59:00 +01:00
release-bot
2fadb6802d refactor: make changelog code simpler 2026-02-08 21:58:01 +01:00
release-bot
9271d3a7e4 fix(app): handle error while parsing commit
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m53s
2026-02-08 21:01:34 +01:00
release-bot
13c83efdb9 fix(app): handle error while parsing changelog 2026-02-08 21:00:30 +01:00
release-bot
e44b73bebf feat: optimize changelog display
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m5s
- Hide releases under a Detail
- Hide all commits under a Detail
2026-02-08 19:04:56 +01:00
979e9fd922 feat: improve changelog readbility
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 2m41s
2026-02-07 17:40:49 +01:00
544500e7fe chore: remove pgp from changelog
All checks were successful
Build & Push CI Image / build-and-push (push) Successful in 8m48s
🚀 Lint & Test & Deploy / release (push) Successful in 4m13s
2026-02-07 16:58:06 +01:00
aaebbc4bc0 fix: some stuff with ci 2026-02-07 16:57:50 +01:00
release-bot
894ab70b79 chore(release): v0.0.3 2026-02-07 15:56:02 +00:00
f8a2a95bc1 chore: clean CHANGELOG.md
Some checks failed
Build & Push CI Image / build-and-push (push) Has been cancelled
🚀 Lint & Test & Deploy / release (push) Successful in 4m14s
2026-02-07 16:32:19 +01:00
c9dd143916 fix(ci): correctly add release notes from tag to changelog 2026-02-07 16:29:59 +01:00
898dd49aee fix(ci): correctly copy changelog to build output
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m7s
2026-02-07 16:20:28 +01:00
9fb69d760f feat: show commits since last release in changelog
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m5s
2026-02-07 16:15:48 +01:00
bafbcca2b8 fix: wrong socket was highlighted when dragging node
The old code had a bug that highlighted a socket from a node to which a
edge already exists which could not be connected to
2026-02-07 16:15:48 +01:00
8ad9e5535c feat: highlight possible sockets when dragging edge
Closes #14
2026-02-07 16:15:44 +01:00
release-bot
43a3c54838 chore(release): v0.0.3 2026-02-07 15:14:21 +00:00
11eaeb719b feat(app): display some git metadata in changelog
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m35s
2026-02-06 16:30:21 +01:00
74c2978cd1 chore: cleanup git.json a bit 2026-02-06 16:16:07 +01:00
4fdc247904 ci: update build.sh to correct git.json
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m21s
2026-02-06 15:57:19 +01:00
c3f8b4b5aa ci: debug available env vars
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m31s
2026-02-06 15:53:08 +01:00
67591c0572 chore: pnpm format
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m3s
2026-02-06 15:46:54 +01:00
de1f9d6ab6 feat(ui): change inputnumber to snap to values when alt is pressed
Some checks failed
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-06 15:44:24 +01:00
6acce72fb8 fix(ui): correctly initialize InputNumber
When the value is outside min/max the value should not be clamped.
2026-02-06 15:25:18 +01:00
cf8943b205 chore: pnpm update 2026-02-06 15:18:32 +01:00
9e03d36482 chore: use newest ci image
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m55s
2026-02-06 15:06:39 +01:00
fd7268d620 ci: make dockerfile work
Some checks failed
Build & Push CI Image / build-and-push (push) Successful in 8m42s
🚀 Lint & Test & Deploy / release (push) Failing after 2m31s
2026-02-06 14:43:31 +01:00
6358c22a85 ci: use tagged own image for ci
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 3m17s
2026-02-06 13:22:33 +01:00
655b6a18b2 ci: make dockerfile work
Some checks failed
Build & Push CI Image / build-and-push (push) Successful in 9m11s
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-06 13:10:41 +01:00
37b2bdc8bd ci: update ci Dockerfile to work
Some checks failed
Build & Push CI Image / build-and-push (push) Failing after 1m24s
🚀 Lint & Test & Deploy / release (push) Failing after 2m23s
2026-02-06 12:52:42 +01:00
94e01d4ea8 ci: correctly build and push ci image
Some checks failed
Build & Push CI Image / build-and-push (push) Failing after 1m0s
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-06 12:50:34 +01:00
35f5177884 feat: try to optimize the Dockerfile
Some checks failed
Build & Push CI Image / build-and-push (push) Failing after 9s
🚀 Lint & Test & Deploy / release (push) Failing after 2m31s
2026-02-06 12:33:36 +01:00
ac2c61f221 ci: use actual git url in ci
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 2m16s
2026-02-06 12:22:51 +01:00
ef3d46279f fix(ci): build before testing 2026-02-06 11:59:43 +01:00
703da324fa ci: automatically build ci image and store locally
Some checks failed
Build & Push CI Image / build-and-push (push) Failing after 3m6s
🚀 Lint & Test & Deploy / release (push) Failing after 1m59s
2026-02-06 11:57:44 +01:00
1dae472253 ci: add a git.json metadata file during build
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 3m14s
2026-02-06 11:48:12 +01:00
09fdfb88cd chore: update test screenshots
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m35s
2026-02-06 00:53:58 +01:00
04b63cc7e2 feat: add changelog to sidebar
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 4m36s
2026-02-06 00:45:33 +01:00
cb6a35606d feat(ci): also cache cargo stuff
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 4m15s
2026-02-04 21:13:10 +01:00
9c9f3ba3b7 fix(ci): use GITHUB_ instead of GITEA_ for env vars
Some checks failed
🚀 Lint & Test & Deploy / release (push) Has been cancelled
2026-02-04 20:39:22 +01:00
08dda2b2cb chore: pnpm format
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 4m46s
2026-02-04 20:30:59 +01:00
059129a738 fix(ci): deploy prs and main
Some checks failed
🚀 Lint & Test & Deploy / release (push) Failing after 1m45s
2026-02-04 20:28:41 +01:00
437c9f4a25 feat(ci): add list of all commits to changelog entry 2026-02-04 20:14:38 +01:00
48bf447ce1 docs: straighten up changelog a bit 2026-02-04 20:08:29 +01:00
548fa4f0a1 fix(app): correctly initialize vec3 inputs in nestedsettings
Closes #32
2026-02-04 20:08:04 +01:00
release-bot
642cca30ad chore(release): v0.0.2 2026-02-04 18:37:09 +00:00
release-bot
419249aca3 chore(release): v0.0.2
Some checks failed
🚀 Release / release (push) Failing after 4m24s
2026-02-03 23:40:36 +00:00
c69cb94ac7 fix(ci): actually deploy on tags
All checks were successful
🚀 Release / release (push) Successful in 4m12s
2026-02-04 00:34:39 +01:00
release-bot
4b652d885f chore(release): v0.0.2 2026-02-03 22:05:51 +00:00
381f784775 fix(app): correctly handle false value in settings
All checks were successful
🚀 Release / release (push) Successful in 4m30s
This caused a bug where random seed could not be false.
2026-02-03 22:46:43 +01:00
91866b4e9a feat/e2e-testing (#31)
All checks were successful
🚀 Release / release (push) Successful in 4m7s
Reviewed-on: #31
Co-authored-by: Max Richter <max@max-richter.dev>
Co-committed-by: Max Richter <max@max-richter.dev>
2026-02-03 22:29:43 +01:00
01f1568221 fix(ci): auto format changelog.md after release
All checks were successful
🚀 Release / release (push) Successful in 3m41s
2026-02-03 15:47:38 +01:00
3e8d2768b3 chore: format
Some checks failed
🚀 Release / release (push) Failing after 1m42s
2026-02-03 15:43:47 +01:00
16a832779a chore(ci): make release script work with sh 2026-02-03 15:43:47 +01:00
d582915842 chore(ci): add jq and git to ci docker image 2026-02-03 15:43:47 +01:00
release-bot
caaecd7a02 chore(release): v0.0.1 2026-02-03 14:43:26 +00:00
93ca436412 fix(ci): make scripts executable
Some checks failed
🚀 Release / release (push) Failing after 3m36s
2026-02-03 15:18:37 +01:00
ecdb986a96 chore(ci): debug some information
Some checks failed
🚀 Release / release (push) Failing after 3m28s
2026-02-03 15:12:47 +01:00
304abf2866 chore(ci): debug some information
Some checks failed
🚀 Release / release (push) Has been cancelled
2026-02-03 15:09:57 +01:00
a547d86946 chore(ci): debug some information
Some checks failed
🚀 Release / release (push) Has been cancelled
2026-02-03 15:09:22 +01:00
667d140883 docs: add information on how to release to readme
All checks were successful
🚀 Release / release (push) Successful in 3m37s
2026-02-03 14:59:12 +01:00
0ac65fd7a7 feat(ci): add release workflow
All checks were successful
🚀 Release / release (push) Successful in 3m34s
2026-02-03 14:52:24 +01:00
Max Richter
5437e062e1 feat(ci): add release workflow 2026-02-03 14:49:14 +01:00
Max Richter
1015d17afb fix(ci): put pnpm store in home instead of workspace
All checks were successful
🏗️ Build and Deploy / build_and_deploy (push) Successful in 3m45s
2026-02-03 13:15:31 +01:00
Max Richter
fd8e5e92d2 chore: run formatting 2026-02-03 13:14:06 +01:00
a2c2503a8e Merge pull request 'feat/ui-float' (#30) from feat/ui-float into main
Some checks failed
🏗️ Build and Deploy / build_and_deploy (push) Failing after 1m40s
Reviewed-on: #30
2026-02-03 13:11:06 +01:00
Max Richter
e18f75e1b8 refactor(ci): make it simpler 2026-02-03 13:10:08 +01:00
Max Richter
6a178dc3a7 fix(ci): correctly copy ui output to app 2026-02-03 13:07:05 +01:00
Max Richter
76cdfee018 feat(app): merge active node and graph settings panel 2026-02-03 13:04:50 +01:00
Max Richter
b19da950a6 refactor: use tailwind custom colors for themes
Use tailwind v4 @theme block so we can use bg-layer-0 instead of
bg-[--layer-0] for theme colors.
2026-02-03 12:18:44 +01:00
Max Richter
89e4cf8364 chore: use vite for auto building @nodarium/ui instead of chokidar
We already use vite for building and during dev. Can use a custom vite
plugin to automatically package ui after every change, so no need for
chokidar.
2026-02-03 12:18:44 +01:00
Max Richter
a28f15c256 chore(ci): simplify ci step
Some checks failed
🏗️ Build and Deploy / setup (push) Successful in 1m5s
🏗️ Build and Deploy / lint (push) Successful in 1m22s
🏗️ Build and Deploy / build_and_deploy (push) Failing after 1m51s
2026-02-02 17:48:11 +01:00
Max Richter
57e3a707c5 chore(ci): simplify ci step
Some checks failed
🏗️ Build and Deploy / setup (push) Successful in 1m12s
🏗️ Build and Deploy / build_and_deploy (push) Has been cancelled
🏗️ Build and Deploy / lint (push) Has been cancelled
2026-02-02 17:46:04 +01:00
Max Richter
dced7db3ad chore(ci): simplify ci step
Some checks failed
🏗️ Build and Deploy / build_and_deploy (push) Has been cancelled
2026-02-02 17:45:04 +01:00
Max Richter
c2dc538c05 fix(ci): make deploy step work
All checks were successful
🏗️ Build and Deploy / setup (push) Successful in 1m11s
🏗️ Build and Deploy / lint (push) Successful in 36s
🏗️ Build and Deploy / format (push) Successful in 40s
🏗️ Build and Deploy / typecheck (push) Successful in 37s
🏗️ Build and Deploy / build_and_deploy (push) Successful in 1m51s
2026-02-02 17:39:17 +01:00
Max Richter
9484b3599e chore: run formatting on all files
Some checks failed
🏗️ Build and Deploy / setup (push) Successful in 1m10s
🏗️ Build and Deploy / lint (push) Successful in 34s
🏗️ Build and Deploy / format (push) Successful in 38s
🏗️ Build and Deploy / typecheck (push) Successful in 33s
🏗️ Build and Deploy / build_and_deploy (push) Failing after 1m53s
2026-02-02 17:33:01 +01:00
Max Richter
3c168aa9b6 chore: add check script to ui package 2026-02-02 17:31:20 +01:00
Max Richter
812099c55d chore: run formatting
Some checks failed
🏗️ Build and Deploy / setup (push) Successful in 1m5s
🏗️ Build and Deploy / lint (push) Successful in 34s
🏗️ Build and Deploy / format (push) Successful in 40s
🏗️ Build and Deploy / typecheck (push) Failing after 12s
🏗️ Build and Deploy / build_and_deploy (push) Has been skipped
2026-02-02 17:07:43 +01:00
Max Richter
025921aeab feat(ci): run on every branch
Some checks failed
🏗️ Build and Deploy / setup (push) Successful in 1m6s
🏗️ Build and Deploy / lint (push) Successful in 51s
🏗️ Build and Deploy / format (push) Failing after 40s
🏗️ Build and Deploy / typecheck (push) Failing after 14s
🏗️ Build and Deploy / build_and_deploy (push) Has been skipped
2026-02-02 16:56:04 +01:00
Max Richter
abaf5245d3 feat(ci): run on every branch
Some checks failed
🏗️ Build and Deploy / lint (push) Has been cancelled
🏗️ Build and Deploy / format (push) Has been cancelled
🏗️ Build and Deploy / typecheck (push) Has been cancelled
🏗️ Build and Deploy / build_and_deploy (push) Has been cancelled
🏗️ Build and Deploy / setup (push) Has been cancelled
2026-02-02 16:51:20 +01:00
Max Richter
a53cee2d5c feat(ci): run on every branch
Some checks failed
🏗️ Build and Deploy / setup (push) Failing after 1m12s
🏗️ Build and Deploy / format (push) Has been skipped
🏗️ Build and Deploy / typecheck (push) Has been skipped
🏗️ Build and Deploy / lint (push) Has been skipped
🏗️ Build and Deploy / build_and_deploy (push) Has been skipped
2026-02-02 16:46:00 +01:00
Max Richter
7d91e53704 feat(ci): run on every branch
Some checks failed
🏗️ Build and Deploy / setup (push) Failing after 1m16s
🏗️ Build and Deploy / lint (push) Has been skipped
🏗️ Build and Deploy / format (push) Has been skipped
🏗️ Build and Deploy / typecheck (push) Has been skipped
🏗️ Build and Deploy / build_and_deploy (push) Has been skipped
2026-02-02 16:41:14 +01:00
Max Richter
18db3cb9f2 Merge branch 'chore/linting'
Some checks failed
🏗️ Build and Deploy / setup (push) Successful in 1m47s
🏗️ Build and Deploy / lint (push) Failing after 2s
🏗️ Build and Deploy / format (push) Failing after 2s
🏗️ Build and Deploy / typecheck (push) Failing after 1s
🏗️ Build and Deploy / build_and_deploy (push) Has been skipped
2026-02-02 16:32:36 +01:00
Max Richter
b6d3269478 feat(ci): run on every branch 2026-02-02 16:32:01 +01:00
7d18c10007 Merge pull request 'chore: setup linting' (#29) from chore/linting into main
Reviewed-on: #29
2026-02-02 16:28:42 +01:00
Max Richter
6f33cdf066 feat(ci): run on every branch 2026-02-02 16:28:08 +01:00
157 changed files with 6020 additions and 2009 deletions

View File

@@ -13,16 +13,12 @@
"markdown": {},
"toml": {},
"dockerfile": {},
"ruff": {},
"jupyter": {},
"malva": {},
"markup": {
// https://dprint.dev/plugins/markup_fmt/config/
"scriptIndent": true,
"styleIndent": true,
},
"yaml": {},
"graphql": {},
"exec": {
"cwd": "${configDir}",
"commands": [
@@ -46,20 +42,18 @@
"**/*-lock.yaml",
"**/yaml.lock",
"**/.DS_Store",
"**/.pnpm-store",
"**/.cargo",
"**/target",
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
"https://plugins.dprint.dev/typescript-0.95.15.wasm",
"https://plugins.dprint.dev/json-0.21.1.wasm",
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
"https://plugins.dprint.dev/markdown-0.21.1.wasm",
"https://plugins.dprint.dev/toml-0.7.0.wasm",
"https://plugins.dprint.dev/dockerfile-0.3.3.wasm",
"https://plugins.dprint.dev/ruff-0.6.11.wasm",
"https://plugins.dprint.dev/jupyter-0.2.1.wasm",
"https://plugins.dprint.dev/g-plane/malva-v0.15.1.wasm",
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
"https://plugins.dprint.dev/g-plane/pretty_graphql-v0.2.3.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm",
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364",
"https://plugins.dprint.dev/biome-0.11.10.wasm",
],
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 39 KiB

45
.gitea/scripts/build.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p app/static
cp CHANGELOG.md app/static/CHANGELOG.md
# Derive branch/tag info
REF_TYPE="${GITHUB_REF_TYPE:-branch}"
REF_NAME="${GITHUB_REF_NAME:-$(basename "$GITHUB_REF")}"
BRANCH="${GITHUB_HEAD_REF:-}"
if [[ -z "$BRANCH" && "$REF_TYPE" == "branch" ]]; then
BRANCH="$REF_NAME"
fi
# Determine last tag and commits since
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
if [[ -n "$LAST_TAG" ]]; then
COMMITS_SINCE_LAST_RELEASE="$(git rev-list --count "${LAST_TAG}..HEAD")"
else
COMMITS_SINCE_LAST_RELEASE="0"
fi
commit_message=$(git log -1 --pretty=%B | tr -d '\n' | sed 's/"/\\"/g')
cat >app/static/git.json <<EOF
{
"ref": "${GITHUB_REF:-}",
"ref_name": "${REF_NAME}",
"ref_type": "${REF_TYPE}",
"sha": "${GITHUB_SHA:-}",
"run_number": "${GITHUB_RUN_NUMBER:-}",
"event_name": "${GITHUB_EVENT_NAME:-}",
"workflow": "${GITHUB_WORKFLOW:-}",
"job": "${GITHUB_JOB:-}",
"commit_message": "${commit_message}",
"commit_timestamp": "$(git log -1 --pretty=%cI)",
"branch": "${BRANCH}",
"commits_since_last_release": "${COMMITS_SINCE_LAST_RELEASE}"
}
EOF
pnpm build
cp -R packages/ui/build app/build/ui

102
.gitea/scripts/create-release.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/sh
set -eu
TAG="$GITHUB_REF_NAME"
VERSION=$(echo "$TAG" | sed 's/^v//')
DATE=$(date +%Y-%m-%d)
echo "🚀 Creating release for $TAG"
# -------------------------------------------------------------------
# 1. Extract release notes from annotated tag
# -------------------------------------------------------------------
# Ensure the local git knows this is an annotated tag with metadata
git fetch origin "refs/tags/$TAG:refs/tags/$TAG" --force
# %(contents) gets the whole message.
# If you want ONLY what you typed after the first line, use %(contents:body)
NOTES=$(git tag -l "$TAG" --format='%(contents)' | sed '/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----/d')
if [ -z "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
echo "❌ Tag message is empty or tag is not annotated"
exit 1
fi
git checkout main
# -------------------------------------------------------------------
# 2. Update all package.json versions
# -------------------------------------------------------------------
echo "🔧 Updating package.json versions to $VERSION"
find . -name package.json ! -path "*/node_modules/*" | while read -r file; do
tmp_file="$file.tmp"
jq --arg v "$VERSION" '.version = $v' "$file" >"$tmp_file"
mv "$tmp_file" "$file"
done
# -------------------------------------------------------------------
# 3. Generate commit list since last release
# -------------------------------------------------------------------
LAST_TAG=$(git tag --sort=-creatordate | grep -v "^$TAG$" | head -n 1 || echo "")
if [ -n "$LAST_TAG" ]; then
# Filter out previous 'chore(release)' commits so the list stays clean
COMMITS=$(git log "$LAST_TAG"..HEAD --pretty=format:'* [%h](https://git.max-richter.dev/max/nodarium/commit/%H) %s' | grep -v "chore(release)")
else
COMMITS=$(git log HEAD --pretty=format:'* [%h](https://git.max-richter.dev/max/nodarium/commit/%H) %s' | grep -v "chore(release)")
fi
# -------------------------------------------------------------------
# 4. Update CHANGELOG.md (prepend)
# -------------------------------------------------------------------
tmp_changelog="CHANGELOG.tmp"
{
echo "# $TAG ($DATE)"
echo ""
echo "$NOTES"
echo ""
if [ -n "$COMMITS" ]; then
echo "---"
echo ""
echo "$COMMITS"
echo ""
fi
echo ""
if [ -f CHANGELOG.md ]; then
cat CHANGELOG.md
fi
} >"$tmp_changelog"
mv "$tmp_changelog" CHANGELOG.md
pnpm exec dprint fmt CHANGELOG.md
# -------------------------------------------------------------------
# 5. Setup GPG signing
# -------------------------------------------------------------------
echo "$BOT_PGP_PRIVATE_KEY" | base64 -d | gpg --batch --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
export GPG_TTY=$(tty)
echo "allow-loopback-pinentry" >>~/.gnupg/gpg-agent.conf
gpg-connect-agent reloadagent /bye
git config user.name "nodarium-bot"
git config user.email "nodarium-bot@max-richter.dev"
git config --global user.signingkey "$GPG_KEY_ID"
git config --global commit.gpgsign true
# -------------------------------------------------------------------
# 6. Create release commit
# -------------------------------------------------------------------
git add CHANGELOG.md $(git ls-files '**/package.json')
if git diff --cached --quiet; then
echo "No changes to commit for release $TAG"
else
git commit -m "chore(release): $TAG"
git push origin main
fi
echo "✅ Release process for $TAG complete"

43
.gitea/scripts/deploy-files.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Configuring rclone"
KEY_FILE="$(mktemp)"
echo "${SSH_PRIVATE_KEY}" >"${KEY_FILE}"
chmod 600 "${KEY_FILE}"
mkdir -p ~/.config/rclone
cat >~/.config/rclone/rclone.conf <<EOF
[sftp-remote]
type = sftp
host = ${SSH_HOST}
user = ${SSH_USER}
port = ${SSH_PORT}
key_file = ${KEY_FILE}
EOF
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
TARGET_DIR="${REMOTE_DIR}"
elif [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then
SAFE_PR_NAME="${GITHUB_HEAD_REF//\//-}"
TARGET_DIR="${REMOTE_DIR}_${SAFE_PR_NAME}"
elif [[ "${GITHUB_REF_NAME:-}" == "main" ]]; then
TARGET_DIR="${REMOTE_DIR}_main"
else
SAFE_REF="${GITHUB_REF_NAME//\//-}"
TARGET_DIR="${REMOTE_DIR}_${SAFE_REF}"
fi
echo "Deploying to ${TARGET_DIR}"
rclone sync \
--update \
--verbose \
--progress \
--exclude _astro/** \
--stats 2s \
--stats-one-line \
--transfers 4 \
./app/build/ \
"sftp-remote:${TARGET_DIR}"

View File

@@ -0,0 +1,41 @@
name: Build & Push CI Image
on:
push:
branches:
- main
paths:
- "Dockerfile.ci"
- ".gitea/workflows/build-ci-image.yaml"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."git.max-richter.dev"]
https = true
insecure = false
- name: Login to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.max-richter.dev
username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ci
push: true
tags: |
git.max-richter.dev/${{ gitea.repository }}-ci:latest
git.max-richter.dev/${{ gitea.repository }}-ci:${{ gitea.sha }}

View File

@@ -1,110 +0,0 @@
name: 🏗️ Build and Deploy
on:
push:
branches: "main"
env:
PNPM_CACHE_FOLDER: .pnpm-store
jobs:
lint:
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- name: 📑 Checkout Code
uses: actions/checkout@v4
- name: 💾 Setup pnpm Cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_CACHE_FOLDER }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
- name: 🧹 Run Linter
run: pnpm lint
format:
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- name: 📑 Checkout Code
uses: actions/checkout@v4
- name: 💾 Setup pnpm Cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_CACHE_FOLDER }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
- name: 🎨 Check Formatting
run: pnpm format:check
typecheck:
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- name: 📑 Checkout Code
uses: actions/checkout@v4
- name: 💾 Setup pnpm Cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_CACHE_FOLDER }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
- name: 🧬 Type Check
run: pnpm check
build_and_deploy:
needs: [lint, format, typecheck]
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- name: 📑 Checkout Code
uses: actions/checkout@v4
- name: 💾 Setup pnpm Cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_CACHE_FOLDER }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
- name: 🛠️ Build Site
run: pnpm run build:deploy
- name: 🔑 Configure rclone
run: |
mkdir -p ~/.config/rclone
cat <<EOF > ~/.config/rclone/rclone.conf
[sftp-remote]
type = sftp
host = ${SSH_HOST}
user = ${SSH_USER}
port = ${SSH_PORT}
key_use_agent = false
key_data = ${SSH_PRIVATE_KEY}
EOF
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ vars.SSH_HOST }}
SSH_PORT: ${{ vars.SSH_PORT }}
SSH_USER: ${{ vars.SSH_USER }}
- name: 🚚 Deploy via rclone
run: |
rclone sync --update -v --progress --exclude "_astro/**" --stats 2s --stats-one-line ./app/build/ sftp-remote:${REMOTE_DIR} --transfers 4
env:
REMOTE_DIR: ${{ vars.REMOTE_DIR }}

View File

@@ -0,0 +1,83 @@
name: 🚀 Lint & Test & Deploy
on:
push:
branches: ["*"]
tags: ["*"]
pull_request:
branches: ["*"]
env:
PNPM_CACHE_FOLDER: .pnpm-store
CARGO_HOME: .cargo
CARGO_TARGET_DIR: target
jobs:
release:
runs-on: ubuntu-latest
container: git.max-richter.dev/max/nodarium-ci:bce06da456e3c008851ac006033cfff256015a47
steps:
- name: 📑 Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN }}
- name: 💾 Setup pnpm Cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_CACHE_FOLDER }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: 🦀 Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
- name: 🧹 Quality Control
run: |
pnpm lint
pnpm format:check
pnpm check
pnpm build
xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" pnpm test
- name: 🚀 Create Release Commit
if: gitea.ref_type == 'tag'
run: ./.gitea/scripts/create-release.sh
env:
BOT_PGP_PRIVATE_KEY: ${{ secrets.BOT_PGP_PRIVATE_KEY }}
- name: 🛠️ Build
run: ./.gitea/scripts/build.sh
- name: 🏷️ Create Gitea Release
if: gitea.ref_type == 'tag'
uses: akkuman/gitea-release-action@v1
with:
tag_name: ${{ gitea.ref_name }}
release_name: Release ${{ gitea.ref_name }}
body_path: CHANGELOG.md
draft: false
prerelease: false
- name: 🚀 Deploy Changed Files via rclone
run: ./.gitea/scripts/deploy-files.sh
env:
REMOTE_DIR: ${{ vars.REMOTE_DIR }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ vars.SSH_HOST }}
SSH_PORT: ${{ vars.SSH_PORT }}
SSH_USER: ${{ vars.SSH_USER }}

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ node_modules/
/target
.direnv/
.pnpm-store/

133
CHANGELOG.md Normal file
View File

@@ -0,0 +1,133 @@
# v0.0.4 (2026-02-10)
## Features
- Added shape and leaf nodes, including rotation support.
- Added high-contrast light theme and improved overall node readability.
- Enhanced UI with dots background, clearer details, and consistent node coloring.
- Improved changelog display and parsing robustness.
## Fixes
- Fixed UI issues (backside rendering, missing types, linter errors).
- Improved CI handling of commit messages and changelog placement.
## Chores
- Simplified CI quality checks.
- Updated dprint linters.
- Refactored changelog code.
---
- [51de3ce](https://git.max-richter.dev/max/nodarium/commit/51de3ced133af07b9432e1137068ef43ddfecbc9) fix(ci): update changelog before building
- [8d403ba](https://git.max-richter.dev/max/nodarium/commit/8d403ba8039a05b687f050993a6afca7fb743e12) Merge pull request 'feat/shape-node' (#36) from feat/shape-node into main
- [6bb3011](https://git.max-richter.dev/max/nodarium/commit/6bb301153ac13c31511b6b28ae95c6e0d4c03e9e) Merge remote-tracking branch 'origin/main' into feat/shape-node
- [02eee5f](https://git.max-richter.dev/max/nodarium/commit/02eee5f9bf4b1bc813d5d28673c4d5d77b392a92) fix: disable macro logs in wasm
- [4f48a51](https://git.max-richter.dev/max/nodarium/commit/4f48a519a950123390530f1b6040e2430a767745) feat(nodes): add rotation to instance node
- [97199ac](https://git.max-richter.dev/max/nodarium/commit/97199ac20fb079d6c157962d1a998d63670d8797) feat(nodes): implement leaf node
- [f36f0cb](https://git.max-richter.dev/max/nodarium/commit/f36f0cb2305692c7be60889bcde7f91179e18b81) feat(ui): show circles only when hovering InputShape
- [ed3d48e](https://git.max-richter.dev/max/nodarium/commit/ed3d48e07fa6db84bbb24db6dbe044cbc36f049f) fix(runtime): correctly encode 2d shape for wasm nodes
- [c610d6c](https://git.max-richter.dev/max/nodarium/commit/c610d6c99152d8233235064b81503c2b0dc4ada8) fix(app): show backside in three instances
- [8865b9b](https://git.max-richter.dev/max/nodarium/commit/8865b9b032bdf5a1385b4e9db0b1923f0e224fdd) feat(node): initial leaf / shape nodes
- [235ee5d](https://git.max-richter.dev/max/nodarium/commit/235ee5d979fbd70b3e0fb6f09a352218c3ff1e6d) fix(app): wrong linter errors in changelog
- [23a4857](https://git.max-richter.dev/max/nodarium/commit/23a48572f3913d91d839873cc155a16139c286a6) feat(app): dots background for node interface
- [e89a46e](https://git.max-richter.dev/max/nodarium/commit/e89a46e146e9e95de57ffdf55b05d16d6fe975f4) feat(app): add error page
- [cefda41](https://git.max-richter.dev/max/nodarium/commit/cefda41fcf3d5d011c9f7598a4f3f37136602dbd) feat(theme): optimize node readability
- [21d0f0d](https://git.max-richter.dev/max/nodarium/commit/21d0f0da5a26492fa68ad4897a9b1d9e88857030) feat: add high-contrast-light theme
- [4620245](https://git.max-richter.dev/max/nodarium/commit/46202451ba3eea73bd1bc6ef5129b3e26ee9315c) ci: simplify ci quality checks
- [0f4239d](https://git.max-richter.dev/max/nodarium/commit/0f4239d179ddedd3d012ca98b5bc3312afbc8f10) ci: simplify ci quality checks
- [d9c9bb5](https://git.max-richter.dev/max/nodarium/commit/d9c9bb5234bc8776daf26be99ba77a2145c70649) fix(theme): allow raw html in head style
- [18802fd](https://git.max-richter.dev/max/nodarium/commit/18802fdc10294a58425f052a4fde4bcf4be58caf) fix(ui): add missing types
- [b1cbd23](https://git.max-richter.dev/max/nodarium/commit/b1cbd235420c99a11154ef6a899cc7e14faf1c37) feat(app): use same color for node outline and header
- [33f10da](https://git.max-richter.dev/max/nodarium/commit/33f10da396fdc13edcb8faaee212280102b24f3a) feat(ui): make details stand out
- [af5b3b2](https://git.max-richter.dev/max/nodarium/commit/af5b3b23ba18d73d6abec60949fb0c9edfc25ff8) fix: make sure that CHANGELOG.md is in correct place
- [64d75b9](https://git.max-richter.dev/max/nodarium/commit/64d75b9686c494075223a0a318297fe59ec99e81) feat(ui): add InputColor and custom theme
- [2e6466c](https://git.max-richter.dev/max/nodarium/commit/2e6466ceca1d2131581d1862e93c756affdf6cd6) chore: update dprint linters
- [20d8e2a](https://git.max-richter.dev/max/nodarium/commit/20d8e2abedf0de30299d947575afef9c8ffd61d9) feat(theme): improve light theme a bit
- [715e1d0](https://git.max-richter.dev/max/nodarium/commit/715e1d095b8a77feb0cf66bbb444baf0f163adcb) feat(theme): merge edge and connection color
- [07e2826](https://git.max-richter.dev/max/nodarium/commit/07e2826f16dafa6a07377c9fb591168fa5c2abcf) feat(ui): improve colors of input shape
- [e0ad97b](https://git.max-richter.dev/max/nodarium/commit/e0ad97b003fd8cb4d950c03e5488a5accf6a37d0) feat(ui): highlight circle on hover on InputShape
- [93df4a1](https://git.max-richter.dev/max/nodarium/commit/93df4a19ff816e2bdfa093594721f0829f84c9e6) fix(ci): handle newline in commit messages for git.json
- [d661a4e](https://git.max-richter.dev/max/nodarium/commit/d661a4e4a9dfa6c9c73b5e24a3edcf56e1bbf48c) feat(ui): improve InputShape ux
- [c7f808c](https://git.max-richter.dev/max/nodarium/commit/c7f808ce2d52925425b49f92edf49d9557f8901d) wip
- [72d6cd6](https://git.max-richter.dev/max/nodarium/commit/72d6cd6ea2886626823e6e86856f19338c7af3c1) feat(ui): add initial InputShape element
- [615f2d3](https://git.max-richter.dev/max/nodarium/commit/615f2d3c4866a9e85f3eca398f3f02100c4df355) feat(ui): allow custom snippets in ui section header
- [2fadb68](https://git.max-richter.dev/max/nodarium/commit/2fadb6802de640d692fdab7d654311df0d7b4836) refactor: make changelog code simpler
- [9271d3a](https://git.max-richter.dev/max/nodarium/commit/9271d3a7e4cb0cc751b635c2adb518de7b4100c7) fix(app): handle error while parsing commit
- [13c83ef](https://git.max-richter.dev/max/nodarium/commit/13c83efdb962a6578ade59f10cc574fef0e17534) fix(app): handle error while parsing changelog
- [e44b73b](https://git.max-richter.dev/max/nodarium/commit/e44b73bebfb1cc8e872cd2fa7d8b6ff3565df374) feat: optimize changelog display
- [979e9fd](https://git.max-richter.dev/max/nodarium/commit/979e9fd92289eba9f77221c563337c00028e4cf5) feat: improve changelog readbility
- [544500e](https://git.max-richter.dev/max/nodarium/commit/544500e7fe9ee14412cef76f3c7a32ba6f291656) chore: remove pgp from changelog
- [aaebbc4](https://git.max-richter.dev/max/nodarium/commit/aaebbc4bc082ee93c2317ce45071c9bc61b0b77e) fix: some stuff with ci
# v0.0.3 (2026-02-07)
## Features
- Edge dragging now highlights valid connection sockets, improving graph editing clarity.
- InputNumber supports snapping to predefined values while holding Alt.
- Changelog is accessible directly from the sidebar and now includes git metadata and a list of commits.
## Fixes
- Fixed incorrect socket highlighting when an edge already existed.
- Corrected initialization of `InputNumber` values outside min/max bounds.
- Fixed initialization of nested vec3 inputs.
- Multiple CI fixes to ensure reliable builds, correct environment variables, and proper image handling.
## Maintenance / CI
- Significant CI and Dockerfile cleanup and optimization.
- Improved git metadata generation during builds.
- Dependency updates, formatting, and test snapshot updates.
---
- [f8a2a95](https://git.max-richter.dev/max/nodarium/commit/f8a2a95bc18fa3c8c1db67dc0c2b66db1ff0d866) chore: clean CHANGELOG.md
- [c9dd143](https://git.max-richter.dev/max/nodarium/commit/c9dd143916d758991f3ba30723a32c18b6f98bb5) fix(ci): correctly add release notes from tag to changelog
- [898dd49](https://git.max-richter.dev/max/nodarium/commit/898dd49aee930350af8645382ef5042765a1fac7) fix(ci): correctly copy changelog to build output
- [9fb69d7](https://git.max-richter.dev/max/nodarium/commit/9fb69d760fdf92ecc2448e468242970ec48443b0) feat: show commits since last release in changelog
- [bafbcca](https://git.max-richter.dev/max/nodarium/commit/bafbcca2b8a7cd9f76e961349f11ec84d1e4da63) fix: wrong socket was highlighted when dragging node
- [8ad9e55](https://git.max-richter.dev/max/nodarium/commit/8ad9e5535cd752ef111504226b4dac57b5adcf3d) feat: highlight possible sockets when dragging edge
- [11eaeb7](https://git.max-richter.dev/max/nodarium/commit/11eaeb719be7f34af8db8b7908008a15308c0cac) feat(app): display some git metadata in changelog
- [74c2978](https://git.max-richter.dev/max/nodarium/commit/74c2978cd16d2dd95ce1ae8019dfb9098e52b4b6) chore: cleanup git.json a bit
- [4fdc247](https://git.max-richter.dev/max/nodarium/commit/4fdc24790490d3f13ee94a557159617f4077a2f9) ci: update build.sh to correct git.json
- [c3f8b4b](https://git.max-richter.dev/max/nodarium/commit/c3f8b4b5aad7a525fb11ab14c9236374cb60442d) ci: debug available env vars
- [67591c0](https://git.max-richter.dev/max/nodarium/commit/67591c0572b873d8c7cd00db8efb7dac2d6d4de2) chore: pnpm format
- [de1f9d6](https://git.max-richter.dev/max/nodarium/commit/de1f9d6ab669b8e699d98b8855e125e21030b5b3) feat(ui): change inputnumber to snap to values when alt is pressed
- [6acce72](https://git.max-richter.dev/max/nodarium/commit/6acce72fb8c416cc7f6eec99c2ae94d6529e960c) fix(ui): correctly initialize InputNumber
- [cf8943b](https://git.max-richter.dev/max/nodarium/commit/cf8943b2059aa286e41865caf75058d35498daf7) chore: pnpm update
- [9e03d36](https://git.max-richter.dev/max/nodarium/commit/9e03d36482bb4f972c384b66b2dcf258f0cd18be) chore: use newest ci image
- [fd7268d](https://git.max-richter.dev/max/nodarium/commit/fd7268d6208aede435e1685817ae6b271c68bd83) ci: make dockerfile work
- [6358c22](https://git.max-richter.dev/max/nodarium/commit/6358c22a853ec340be5223fabb8289092e4f4afe) ci: use tagged own image for ci
- [655b6a1](https://git.max-richter.dev/max/nodarium/commit/655b6a18b282f0cddcc750892e575ee6c311036b) ci: make dockerfile work
- [37b2bdc](https://git.max-richter.dev/max/nodarium/commit/37b2bdc8bdbd8ded6b22b89214b49de46f788351) ci: update ci Dockerfile to work
- [94e01d4](https://git.max-richter.dev/max/nodarium/commit/94e01d4ea865f15ce06b52827a1ae6906de5be5e) ci: correctly build and push ci image
- [35f5177](https://git.max-richter.dev/max/nodarium/commit/35f5177884b62bbf119af1bbf4df61dd0291effb) feat: try to optimize the Dockerfile
- [ac2c61f](https://git.max-richter.dev/max/nodarium/commit/ac2c61f2211ba96bbdbb542179905ca776537cec) ci: use actual git url in ci
- [ef3d462](https://git.max-richter.dev/max/nodarium/commit/ef3d46279f4ff9c04d80bb2d9a9e7cfec63b224e) fix(ci): build before testing
- [703da32](https://git.max-richter.dev/max/nodarium/commit/703da324fabbef0e2c017f0f7a925209fa26bd03) ci: automatically build ci image and store locally
- [1dae472](https://git.max-richter.dev/max/nodarium/commit/1dae472253ccb5e3766f2270adc053b922f46738) ci: add a git.json metadata file during build
- [09fdfb8](https://git.max-richter.dev/max/nodarium/commit/09fdfb88cd203ace0e36663ebdb2c8c7ba53f190) chore: update test screenshots
- [04b63cc](https://git.max-richter.dev/max/nodarium/commit/04b63cc7e2fc4fcfa0973cf40592d11457179db3) feat: add changelog to sidebar
- [cb6a356](https://git.max-richter.dev/max/nodarium/commit/cb6a35606dfda50b0c81b04902d7a6c8e59458d2) feat(ci): also cache cargo stuff
- [9c9f3ba](https://git.max-richter.dev/max/nodarium/commit/9c9f3ba3b7c94215a86b0a338a5cecdd87b96b28) fix(ci): use GITHUB_instead of GITEA_ for env vars
- [08dda2b](https://git.max-richter.dev/max/nodarium/commit/08dda2b2cb4d276846abe30bc260127626bb508a) chore: pnpm format
- [059129a](https://git.max-richter.dev/max/nodarium/commit/059129a738d02b8b313bb301a515697c7c4315ac) fix(ci): deploy prs and main
- [437c9f4](https://git.max-richter.dev/max/nodarium/commit/437c9f4a252125e1724686edace0f5f006f58439) feat(ci): add list of all commits to changelog entry
- [48bf447](https://git.max-richter.dev/max/nodarium/commit/48bf447ce12949d7c29a230806d160840b7847e1) docs: straighten up changelog a bit
- [548fa4f](https://git.max-richter.dev/max/nodarium/commit/548fa4f0a1a14adc40a74da1182fa6da81eab3df) fix(app): correctly initialize vec3 inputs in nestedsettings
# v0.0.2 (2026-02-04)
## Fixes
---
- []() fix(ci): actually deploy on tags
- []() fix(app): correctly handle false value in settings
# v0.0.1 (2026-02-03)
chore: format

16
Cargo.lock generated
View File

@@ -62,6 +62,14 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "leaf"
version = "0.1.0"
dependencies = [
"nodarium_macros",
"nodarium_utils",
]
[[package]]
name = "math"
version = "0.1.0"
@@ -245,6 +253,14 @@ dependencies = [
"zmij",
]
[[package]]
name = "shape"
version = "0.1.0"
dependencies = [
"nodarium_macros",
"nodarium_utils",
]
[[package]]
name = "stem"
version = "0.1.0"

View File

@@ -1,15 +0,0 @@
FROM node:24-alpine
RUN apk add --no-cache --update curl rclone g++
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN curl --silent --show-error --location --fail --retry 3 \
--proto '=https' --tlsv1.2 \
--output /tmp/rustup-init.sh https://sh.rustup.rs \
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
&& rm /tmp/rustup-init.sh \
&& rustup target add wasm32-unknown-unknown \
&& npm i -g pnpm

32
Dockerfile.ci Normal file
View File

@@ -0,0 +1,32 @@
FROM node:25-bookworm-slim
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN apt-get update && apt-get install -y \
ca-certificates=20230311+deb12u1 \
gpg=2.2.40-1.1+deb12u2 \
gpg-agent=2.2.40-1.1+deb12u2 \
curl=7.88.1-10+deb12u14 \
git=1:2.39.5-0+deb12u3 \
jq=1.6-2.1+deb12u1 \
g++=4:12.2.0-3 \
rclone=1.60.1+dfsg-2+b5 \
xvfb=2:21.1.7-3+deb12u11 \
xauth=1:1.1.2-1 \
--no-install-recommends \
# Install Rust
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
--default-toolchain stable \
--profile minimal \
&& rustup target add wasm32-unknown-unknown \
# Setup Playwright
&& npm i -g pnpm \
&& pnpm dlx playwright install --with-deps firefox \
# Final Cleanup
&& rm -rf /usr/local/rustup/downloads /usr/local/rustup/tmp \
&& rm -rf /usr/local/cargo/registry/index /usr/local/cargo/registry/cache \
&& rm -rf /usr/local/rustup/toolchains/*/share/doc \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*

View File

@@ -4,11 +4,11 @@ Nodarium
<a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a>
<p align="center">
<p align="center">
Nodarium is a WebAssembly based visual programming language.
</p>
<img src=".gitea/graphics/nodes.svg" width="80%"/>
<img src=".gitea/graphics/nodes.svg" width="80%"/>
</div>
@@ -50,4 +50,29 @@ pnpm dev
### [Now you can create your first node 🤓](./docs/DEVELOPING_NODES.md)
# Releasing
## Creating a Release
1. **Create an annotated tag** with your release notes:
```bash
git tag -a v1.0.0 -m "Release notes for this version"
git push origin v1.0.0
```
2. **The CI workflow will:**
- Run lint, format check, and type check
- Build the project
- Update all `package.json` versions to match the tag
- Generate/update `CHANGELOG.md`
- Create a release commit on `main`
- Publish a Gitea release
## Version Requirements
- Tag must match pattern `v*` (e.g., `v1.0.0`, `v2.3.1`)
- Tag message must not be empty (annotated tag required)
- Tag must be pushed from `main` branch
# Roadmap

2
app/.gitignore vendored
View File

@@ -27,3 +27,5 @@ dist-ssr
*.sln
*.sw?
build/
test-results/

62
app/e2e/main.test.ts Normal file
View File

@@ -0,0 +1,62 @@
import { expect, test } from '@playwright/test';
test('test', async ({ page }) => {
// Listen for console messages
page.on('console', msg => {
console.log(`[Browser Console] ${msg.type()}: ${msg.text()}`);
});
await page.goto('http://localhost:4173', { waitUntil: 'load' });
// await expect(page).toHaveScreenshot();
await expect(page.locator('.graph-wrapper')).toHaveScreenshot();
await page.getByRole('button', { name: 'projects' }).click();
await page.getByRole('button', { name: 'New', exact: true }).click();
await page.getByRole('combobox').selectOption('2');
await page.getByRole('textbox', { name: 'Project name' }).click();
await page.getByRole('textbox', { name: 'Project name' }).fill('Test Project');
await page.getByRole('button', { name: 'Create' }).click();
const expectedNodes = [
{
id: '10',
type: 'max/plantarium/stem',
props: {
amount: 50,
length: 4,
thickness: 1
}
},
{
id: '11',
type: 'max/plantarium/noise',
props: {
scale: 0.5,
strength: 5
}
},
{
id: '9',
type: 'max/plantarium/output'
}
];
for (const node of expectedNodes) {
const wrapper = page.locator(
`div.wrapper[data-node-id="${node.id}"][data-node-type="${node.type}"]`
);
await expect(wrapper).toBeVisible();
if ('props' in node) {
const props = node.props as unknown as Record<string, number>;
for (const propId in node.props) {
const expectedValue = props[propId];
const inputElement = page.locator(
`div.wrapper[data-node-type="${node.type}"][data-node-input="${propId}"] input[type="number"]`
);
const value = parseFloat(await inputElement.inputValue());
expect(value).toBe(expectedValue);
}
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,12 +1,15 @@
{
"name": "@nodarium/app",
"private": true,
"version": "0.0.0",
"version": "0.0.4",
"type": "module",
"scripts": {
"dev": "vite dev",
"predev": "rm static/CHANGELOG.md && ln -s ../../CHANGELOG.md static/CHANGELOG.md",
"build": "svelte-kit sync && vite build",
"test": "vitest",
"test:unit": "vitest",
"test": "npm run test:unit -- --run && npm run test:e2e",
"test:e2e": "playwright test",
"preview": "vite preview",
"format": "dprint fmt -c '../.dprint.jsonc' .",
"format:check": "dprint check -c '../.dprint.jsonc' .",
@@ -16,7 +19,7 @@
"dependencies": {
"@nodarium/ui": "workspace:*",
"@nodarium/utils": "workspace:*",
"@sveltejs/kit": "^2.50.0",
"@sveltejs/kit": "^2.50.2",
"@tailwindcss/vite": "^4.1.18",
"@threlte/core": "8.3.1",
"@threlte/extras": "9.7.1",
@@ -24,27 +27,29 @@
"file-saver": "^2.0.5",
"idb": "^8.0.3",
"jsondiffpatch": "^0.7.3",
"micromark": "^4.0.2",
"tailwindcss": "^4.1.18",
"three": "^0.182.0",
"wabt": "^1.0.39"
"three": "^0.182.0"
},
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@iconify-json/tabler": "^1.2.26",
"@iconify/tailwind4": "^1.2.1",
"@nodarium/types": "workspace:",
"@nodarium/types": "workspace:^",
"@playwright/test": "^1.58.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.6",
"@tsconfig/svelte": "^5.0.7",
"@types/file-saver": "^2.0.7",
"@types/three": "^0.182.0",
"@vitest/browser-playwright": "^4.0.18",
"dprint": "^0.51.1",
"eslint": "^9.39.2",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.3.0",
"svelte": "^5.46.4",
"svelte-check": "^4.3.5",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
@@ -52,6 +57,7 @@
"vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.5",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.17"
"vitest": "^4.0.18",
"vitest-browser-svelte": "^2.0.2"
}
}

20
app/playwright.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: { command: 'pnpm build && pnpm preview', port: 4173 },
testDir: 'e2e',
use: {
browserName: 'firefox',
launchOptions: {
firefoxUserPrefs: {
// Force WebGL even without a GPU
'webgl.force-enabled': true,
'webgl.disabled': false,
// Use software rendering (Mesa) instead of hardware
'layers.acceleration.disabled': true,
'gfx.webrender.software': true,
'webgl.enable-webgl2': true
}
}
}
});

View File

@@ -2,5 +2,9 @@
@source "../../packages/ui/**/*.svelte";
@plugin "@iconify/tailwind4" {
prefix: "i";
icon-sets: from-folder(custom, "./src/lib/icons");
icon-sets: from-folder("custom", "./src/lib/icons");
}
body * {
color: var(--color-text);
}

View File

@@ -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>

View File

@@ -11,6 +11,7 @@ uniform vec3 camPos;
uniform vec2 zoomLimits;
uniform vec3 backgroundColor;
uniform vec3 lineColor;
uniform int gridType; // 0 = grid lines, 1 = dots
// Anti-aliased step: threshold in the same units as `value`
float aaStep(float threshold, float value, float deriv) {
@@ -78,35 +79,51 @@ void main(void) {
float ux = (vUv.x - 0.5) * width + cx * cz;
float uy = (vUv.y - 0.5) * height - cy * cz;
// extra small grid
float m1 = grid(ux, uy, divisions * 4.0, thickness * 4.0) * 0.9;
float m2 = grid(ux, uy, divisions * 16.0, thickness * 16.0) * 0.5;
float xsmall = max(m1, m2);
float s3 = circle_grid(ux, uy, cz / 1.6, 1.0) * 0.5;
xsmall = max(xsmall, s3);
if(gridType == 0) {
// extra small grid
float m1 = grid(ux, uy, divisions * 4.0, thickness * 4.0) * 0.9;
float m2 = grid(ux, uy, divisions * 16.0, thickness * 16.0) * 0.5;
float xsmall = max(m1, m2);
float s3 = circle_grid(ux, uy, cz / 1.6, 1.0) * 0.5;
xsmall = max(xsmall, s3);
// small grid
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
float c2 = grid(ux, uy, divisions * 2.0, thickness * 2.0) * 0.5;
float small = max(c1, c2);
// small grid
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
float c2 = grid(ux, uy, divisions * 2.0, thickness * 2.0) * 0.5;
float small = max(c1, c2);
float s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
small = max(small, s1);
float s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
small = max(small, s1);
// large grid
float c3 = grid(ux, uy, divisions / 8.0, thickness / 8.0) * 0.5;
float c4 = grid(ux, uy, divisions / 2.0, thickness / 4.0) * 0.4;
float large = max(c3, c4);
// large grid
float c3 = grid(ux, uy, divisions / 8.0, thickness / 8.0) * 0.5;
float c4 = grid(ux, uy, divisions / 2.0, thickness / 4.0) * 0.4;
float large = max(c3, c4);
float s2 = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
large = max(large, s2);
float s2 = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
large = max(large, s2);
float c = mix(large, small, min(nz * 2.0 + 0.05, 1.0));
c = mix(c, xsmall, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
float c = mix(large, small, min(nz * 2.0 + 0.05, 1.0));
c = mix(c, xsmall, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
vec3 color = mix(backgroundColor, lineColor, c);
vec3 color = mix(backgroundColor, lineColor, c);
gl_FragColor = vec4(color, 1.0);
} else {
float large = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
float medium = circle_grid(ux, uy, cz * 10.0, 1.0) * 0.6;
float small = circle_grid(ux, uy, cz * 2.5, 1.0) * 0.8;
float c = mix(large, medium, min(nz * 2.0 + 0.05, 1.0));
c = mix(c, small, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
vec3 color = mix(backgroundColor, lineColor, c);
gl_FragColor = vec4(color, 1.0);
}
gl_FragColor = vec4(color, 1.0);
}

View File

@@ -1,16 +1,17 @@
<script lang="ts">
import { appSettings } from "$lib/settings/app-settings.svelte";
import { T } from "@threlte/core";
import { colors } from "../graph/colors.svelte";
import BackgroundFrag from "./Background.frag";
import BackgroundVert from "./Background.vert";
import { appSettings } from '$lib/settings/app-settings.svelte';
import { T } from '@threlte/core';
import { colors } from '../graph/colors.svelte';
import BackgroundFrag from './Background.frag';
import BackgroundVert from './Background.vert';
type Props = {
minZoom: number;
maxZoom: number;
cameraPosition: [number, number, number];
width: number;
height: number;
minZoom?: number;
maxZoom?: number;
cameraPosition?: [number, number, number];
width?: number;
height?: number;
type?: 'grid' | 'dots' | 'none';
};
let {
@@ -19,8 +20,17 @@
cameraPosition = [0, 1, 0],
width = globalThis?.innerWidth || 100,
height = globalThis?.innerHeight || 100,
type = 'grid'
}: Props = $props();
const typeMap = new Map([
['grid', 0],
['dots', 1],
['none', 2]
]);
const gridType = $derived(typeMap.get(type) || 0);
let bw = $derived(width / cameraPosition[2]);
let bh = $derived(height / cameraPosition[2]);
</script>
@@ -38,27 +48,31 @@
fragmentShader={BackgroundFrag}
uniforms={{
camPos: {
value: [0, 1, 0],
value: [0, 1, 0]
},
backgroundColor: {
value: colors["layer-0"],
value: colors['layer-0']
},
lineColor: {
value: colors["outline"],
value: colors['outline']
},
zoomLimits: {
value: [2, 50],
value: [2, 50]
},
dimensions: {
value: [100, 100],
value: [100, 100]
},
gridType: {
value: 0
}
}}
uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={appSettings.value.theme &&
colors["layer-0"]}
uniforms.lineColor.value={appSettings.value.theme && colors["outline"]}
uniforms.backgroundColor.value={appSettings.value.theme
&& colors['layer-0']}
uniforms.lineColor.value={appSettings.value.theme && colors['outline']}
uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]}
uniforms.gridType.value={gridType}
/>
</T.Mesh>
</T.Group>

View File

@@ -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"
@@ -149,7 +196,7 @@
}
input {
background: var(--layer-0);
background: var(--color-layer-0);
font-family: var(--font-family);
border: none;
border-radius: 5px;
@@ -168,10 +215,10 @@
.add-menu-wrapper {
position: absolute;
background: var(--layer-1);
background: var(--color-layer-1);
border-radius: 7px;
overflow: hidden;
border: solid 2px var(--layer-2);
border: solid 2px var(--color-layer-2);
width: 150px;
}
.content {
@@ -184,14 +231,14 @@
.result {
padding: 1em 0.9em;
border-bottom: solid 1px var(--layer-2);
border-bottom: solid 1px var(--color-layer-2);
opacity: 0.7;
font-size: 0.9em;
cursor: pointer;
}
.result[aria-selected="true"] {
background: var(--layer-2);
background: var(--color-layer-2);
opacity: 1;
}
</style>

View File

@@ -32,7 +32,7 @@
.box-selection {
width: 40px;
height: 20px;
border: solid 2px var(--outline);
border: solid 2px var(--color-outline);
border-style: dashed;
border-radius: 2px;
}

View File

@@ -88,12 +88,12 @@
position: fixed;
pointer-events: none;
transform: translate(var(--mx), var(--my));
background: var(--layer-1);
background: var(--color-layer-1);
border-radius: 5px;
top: 10px;
left: 10px;
max-width: 250px;
border: 1px solid var(--outline);
border: 1px solid var(--color-outline);
z-index: 10000;
display: none;
}

View File

@@ -2,19 +2,16 @@
import { colors } from '../graph/colors.svelte';
const circleMaterial = new MeshBasicMaterial({
color: colors.edge.clone(),
color: colors.outline.clone(),
toneMapped: false
});
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
$effect.root(() => {
$effect(() => {
if (appSettings.value.theme === undefined) {
return;
}
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
lineColor = colors.edge.clone().convertSRGBToLinear();
circleMaterial.color = 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>

View File

@@ -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>

View File

@@ -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);
}

View 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;
};

View File

@@ -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);
}

View File

@@ -0,0 +1,265 @@
import { describe, expect, it } from 'vitest';
import { GraphManager } from './graph-manager.svelte';
import {
createMockNodeRegistry,
mockFloatInputNode,
mockFloatOutputNode,
mockGeometryOutputNode,
mockPathInputNode,
mockVec3OutputNode
} from './test-utils';
describe('GraphManager', () => {
describe('getPossibleSockets', () => {
describe('when dragging an output socket', () => {
it('should return compatible input sockets based on type', () => {
const registry = createMockNodeRegistry([
mockFloatOutputNode,
mockFloatInputNode,
mockGeometryOutputNode,
mockPathInputNode
]);
const manager = new GraphManager(registry);
const floatInputNode = manager.createNode({
type: 'test/node/input',
position: [100, 100],
props: {}
});
const floatOutputNode = manager.createNode({
type: 'test/node/output',
position: [0, 0],
props: {}
});
expect(floatInputNode).toBeDefined();
expect(floatOutputNode).toBeDefined();
const possibleSockets = manager.getPossibleSockets({
node: floatOutputNode!,
index: 0,
position: [0, 0]
});
expect(possibleSockets.length).toBe(1);
const socketNodeIds = possibleSockets.map(([node]) => node.id);
expect(socketNodeIds).toContain(floatInputNode!.id);
});
it('should exclude self node from possible sockets', () => {
const registry = createMockNodeRegistry([
mockFloatOutputNode,
mockFloatInputNode
]);
const manager = new GraphManager(registry);
const floatInputNode = manager.createNode({
type: 'test/node/input',
position: [100, 100],
props: {}
});
expect(floatInputNode).toBeDefined();
const possibleSockets = manager.getPossibleSockets({
node: floatInputNode!,
index: 'value',
position: [0, 0]
});
const socketNodeIds = possibleSockets.map(([node]) => node.id);
expect(socketNodeIds).not.toContain(floatInputNode!.id);
});
it('should exclude parent nodes from possible sockets when dragging output', () => {
const registry = createMockNodeRegistry([
mockFloatOutputNode,
mockFloatInputNode
]);
const manager = new GraphManager(registry);
const parentNode = manager.createNode({
type: 'test/node/output',
position: [0, 0],
props: {}
});
const childNode = manager.createNode({
type: 'test/node/input',
position: [100, 100],
props: {}
});
expect(parentNode).toBeDefined();
expect(childNode).toBeDefined();
if (parentNode && childNode) {
manager.createEdge(parentNode, 0, childNode, 'value');
}
const possibleSockets = manager.getPossibleSockets({
node: parentNode!,
index: 0,
position: [0, 0]
});
const socketNodeIds = possibleSockets.map(([node]) => node.id);
expect(socketNodeIds).not.toContain(childNode!.id);
});
it('should return sockets compatible with accepts property', () => {
const registry = createMockNodeRegistry([
mockGeometryOutputNode,
mockPathInputNode
]);
const manager = new GraphManager(registry);
const geometryOutputNode = manager.createNode({
type: 'test/node/geometry',
position: [0, 0],
props: {}
});
const pathInputNode = manager.createNode({
type: 'test/node/path',
position: [100, 100],
props: {}
});
expect(geometryOutputNode).toBeDefined();
expect(pathInputNode).toBeDefined();
const possibleSockets = manager.getPossibleSockets({
node: geometryOutputNode!,
index: 0,
position: [0, 0]
});
const socketNodeIds = possibleSockets.map(([node]) => node.id);
expect(socketNodeIds).toContain(pathInputNode!.id);
});
it('should return empty array when no compatible sockets exist', () => {
const registry = createMockNodeRegistry([
mockVec3OutputNode,
mockFloatInputNode
]);
const manager = new GraphManager(registry);
const vec3OutputNode = manager.createNode({
type: 'test/node/vec3',
position: [0, 0],
props: {}
});
const floatInputNode = manager.createNode({
type: 'test/node/input',
position: [100, 100],
props: {}
});
expect(vec3OutputNode).toBeDefined();
expect(floatInputNode).toBeDefined();
const possibleSockets = manager.getPossibleSockets({
node: vec3OutputNode!,
index: 0,
position: [0, 0]
});
const socketNodeIds = possibleSockets.map(([node]) => node.id);
expect(socketNodeIds).not.toContain(floatInputNode!.id);
expect(possibleSockets.length).toBe(0);
});
it('should return socket info with correct socket key for inputs', () => {
const registry = createMockNodeRegistry([
mockFloatOutputNode,
mockFloatInputNode
]);
const manager = new GraphManager(registry);
const floatOutputNode = manager.createNode({
type: 'test/node/output',
position: [0, 0],
props: {}
});
const floatInputNode = manager.createNode({
type: 'test/node/input',
position: [100, 100],
props: {}
});
expect(floatOutputNode).toBeDefined();
expect(floatInputNode).toBeDefined();
const possibleSockets = manager.getPossibleSockets({
node: floatOutputNode!,
index: 0,
position: [0, 0]
});
const matchingSocket = possibleSockets.find(([node]) => node.id === floatInputNode!.id);
expect(matchingSocket).toBeDefined();
expect(matchingSocket![1]).toBe('value');
});
it('should return multiple compatible sockets', () => {
const registry = createMockNodeRegistry([
mockFloatOutputNode,
mockFloatInputNode,
mockGeometryOutputNode,
mockPathInputNode
]);
const manager = new GraphManager(registry);
const floatOutputNode = manager.createNode({
type: 'test/node/output',
position: [0, 0],
props: {}
});
const geometryOutputNode = manager.createNode({
type: 'test/node/geometry',
position: [200, 0],
props: {}
});
const floatInputNode = manager.createNode({
type: 'test/node/input',
position: [100, 100],
props: {}
});
const pathInputNode = manager.createNode({
type: 'test/node/path',
position: [300, 100],
props: {}
});
expect(floatOutputNode).toBeDefined();
expect(geometryOutputNode).toBeDefined();
expect(floatInputNode).toBeDefined();
expect(pathInputNode).toBeDefined();
const possibleSocketsForFloat = manager.getPossibleSockets({
node: floatOutputNode!,
index: 0,
position: [0, 0]
});
expect(possibleSocketsForFloat.length).toBe(1);
expect(possibleSocketsForFloat.map(([n]) => n.id)).toContain(floatInputNode!.id);
});
});
});
});

View File

@@ -29,8 +29,9 @@ 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;
}
@@ -757,12 +758,16 @@ export class GraphManager extends EventEmitter<{
(n) => n.id !== node.id && !parents.has(n.id)
);
// get edges from this socket
const edges = new SvelteMap(
this.getEdgesFromNode(node)
.filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]])
);
const edges = new SvelteMap<number, string[]>();
this.getEdgesFromNode(node)
.filter((e) => e[1] === index)
.forEach((e) => {
if (edges.has(e[2].id)) {
edges.get(e[2].id)?.push(e[3]);
} else {
edges.set(e[2].id, [e[3]]);
}
});
const ownType = nodeType.outputs?.[index];
@@ -775,7 +780,7 @@ export class GraphManager extends EventEmitter<{
if (
areSocketsCompatible(ownType, otherType)
&& edges.get(node.id) !== key
&& !edges.get(node.id)?.includes(key)
) {
sockets.push([node, key]);
}

View File

@@ -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(() => {
@@ -83,7 +110,7 @@ export class GraphState {
addMenuPosition = $state<[number, number] | null>(null);
snapToGrid = $state(false);
showGrid = $state(true);
backgroundType = $state<'grid' | 'dots' | 'none'>('grid');
showHelp = $state(false);
cameraDown = [0, 0];
@@ -159,44 +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
];
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;
}
}
}
private nodeHeightCache: Record<string, number> = {};
getNodeHeight(nodeTypeId: string) {
if (nodeTypeId in this.nodeHeightCache) {
return this.nodeHeightCache[nodeTypeId];
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');
}
const node = this.graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 5;
}
const height = 5
+ 10
* Object.keys(node.inputs).filter(
(p) =>
p !== 'seed'
&& node?.inputs
&& !(node?.inputs?.[p] !== undefined && 'setting' in node.inputs[p])
&& node.inputs[p].hidden !== true
).length;
this.nodeHeightCache[nodeTypeId] = height;
return height;
}
copyNodes() {
@@ -256,7 +266,7 @@ export class GraphState {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = this.getSocketPosition(node, index);
position = getSocketPosition(node, index);
this.graph.removeEdge(edge);
break;
}
@@ -276,7 +286,7 @@ export class GraphState {
return {
node,
index,
position: this.getSocketPosition(node, index)
position: getSocketPosition(node, index)
};
});
}
@@ -313,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;
@@ -325,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() {

View File

@@ -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
@@ -132,8 +142,9 @@
position={graphState.cameraPosition}
/>
{#if graphState.showGrid !== false}
{#if graphState.backgroundType !== 'none'}
<Background
type={graphState.backgroundType}
cameraPosition={graphState.cameraPosition}
{maxZoom}
{minZoom}
@@ -159,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]}
@@ -177,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}
@@ -199,7 +220,6 @@
<NodeEl
{node}
inView={graphState.isNodeInView(node)}
z={graphState.cameraPosition[2]}
/>
{/each}
</div>
@@ -244,7 +264,7 @@
z-index: 1;
width: 100%;
height: 100%;
background: var(--layer-2);
background: var(--color-layer-2);
opacity: 0;
}
input:disabled {
@@ -264,8 +284,8 @@
border-radius: 5px;
width: calc(100% - 20px);
height: calc(100% - 25px);
border: dashed 4px var(--layer-2);
background: var(--layer-1);
border: dashed 4px var(--color-layer-2);
background: var(--color-layer-1);
opacity: 0.5;
}
</style>

View File

@@ -13,11 +13,13 @@
settings?: Record<string, unknown>;
activeNode?: NodeInstance;
showGrid?: boolean;
backgroundType?: 'grid' | 'dots' | 'none';
snapToGrid?: boolean;
showHelp?: boolean;
settingTypes?: Record<string, unknown>;
addMenuPadding?: { left?: number; right?: number; bottom?: number; top?: number };
onsave?: (save: Graph) => void;
onresult?: (result: unknown) => void;
};
@@ -25,9 +27,10 @@
let {
graph,
registry,
addMenuPadding,
settings = $bindable(),
activeNode = $bindable(),
showGrid = $bindable(true),
backgroundType = $bindable('grid'),
snapToGrid = $bindable(true),
showHelp = $bindable(false),
settingTypes = $bindable(),
@@ -43,7 +46,7 @@
const graphState = new GraphState(manager);
$effect(() => {
graphState.showGrid = showGrid;
graphState.backgroundType = backgroundType;
graphState.snapToGrid = snapToGrid;
graphState.showHelp = showHelp;
});
@@ -83,4 +86,4 @@
});
</script>
<GraphEl {keymap} />
<GraphEl {keymap} {addMenuPadding} />

View File

@@ -9,12 +9,12 @@ const variables = [
'outline',
'active',
'selected',
'edge'
'connection'
] as const;
function getColor(variable: (typeof variables)[number]) {
const style = getComputedStyle(document.body.parentElement!);
const color = style.getPropertyValue(`--${variable}`);
const color = style.getPropertyValue(`--color-${variable}`);
return new Color().setStyle(color, LinearSRGBColorSpace);
}
@@ -27,7 +27,7 @@ $effect.root(() => {
if (!appSettings.value.theme || !('getComputedStyle' in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!);
for (const v of variables) {
const hex = style.getPropertyValue(`--${v}`);
const hex = style.getPropertyValue(`--color-${v}`);
colors[v].setStyle(hex, LinearSRGBColorSpace);
}
});

View 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}%)`;
}
}

View File

@@ -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';
@@ -166,15 +167,14 @@ export class MouseEventManager {
if (this.state.mouseDown) return;
this.state.edgeEndPosition = null;
const target = event.target as HTMLElement;
if (event.target instanceof HTMLElement) {
if (
event.target.nodeName !== 'CANVAS'
&& !event.target.classList.contains('node')
&& !event.target.classList.contains('content')
) {
return;
}
if (
target.nodeName !== 'CANVAS'
&& !target.classList.contains('node')
&& !target.classList.contains('content')
) {
return;
}
const mx = event.clientX - this.state.rect.x;
@@ -189,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
@@ -265,7 +269,7 @@ export class MouseEventManager {
}
}
if (_socket && smallestDist < 0.9) {
if (_socket && smallestDist < 1.5) {
this.state.mousePosition = _socket.position;
this.state.hoveredSocket = _socket;
} else {
@@ -290,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 {

View File

@@ -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, ' ');

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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
@@ -58,13 +73,18 @@
uColorBright: { value: colors['layer-2'] },
uColorDark: { value: colors['layer-1'] },
uStrokeColor: { value: colors.outline.clone() },
uStrokeWidth: { value: 1.0 },
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} />

View File

@@ -71,22 +71,22 @@
user-select: none !important;
cursor: pointer;
width: 200px;
color: var(--text-color);
color: var(--color-text);
transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1;
opacity: calc((var(--cz) - 2.5) / 3.5);
font-weight: 300;
--stroke: var(--outline);
--stroke: var(--color-outline);
--stroke-width: 2px;
}
.node.active {
--stroke: var(--active);
--stroke: var(--color-active);
--stroke-width: 2px;
}
.node.selected {
--stroke: var(--selected);
--stroke: var(--color-selected);
--stroke-width: 2px;
}
</style>

View File

@@ -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)
});
}
}
@@ -35,22 +37,43 @@
);
const pathHover = $derived(
createNodePath({
depth: 8.5,
height: 50,
depth: 7,
height: 40,
y: 49,
cornerTop,
rightBump,
aspectRatio
})
);
const socketId = $derived(`${node.id}-${0}`);
function getSocketType(s: Socket | null) {
if (!s) return 'unknown';
if (typeof s.index === 'string') {
return s.node.state.type?.inputs?.[s.index].type || 'unknown';
}
return s.node.state.type?.outputs?.[s.index] || 'unknown';
}
const socketType = $derived(getSocketType(graphState.activeSocket));
const hoverColor = $derived(graphState.colors.getColor(socketType));
</script>
<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%;
@@ -87,11 +123,9 @@
width: 30px;
z-index: 100;
border-radius: 50%;
/* background: red; */
/* opacity: 0.2; */
}
.click-target:hover + svg path {
.target:hover + svg path {
d: var(--hover-path);
}
@@ -108,11 +142,16 @@
svg path {
stroke-width: 0.2px;
transition: d 0.3s ease, fill 0.3s ease;
fill: var(--layer-2);
transition:
d 0.3s ease,
fill 0.3s ease;
fill: var(--color-layer-2);
stroke: var(--stroke);
stroke-width: var(--stroke-width);
d: var(--path);
stroke-linejoin: round;
shape-rendering: geometricPrecision;
}
.content {

View File

@@ -31,11 +31,24 @@
return 0;
}
let value = $state(getDefaultValue());
let value = $state(structuredClone($state.snapshot(getDefaultValue())));
function diffArray(a: number[], b?: number[] | number) {
if (!Array.isArray(b)) return true;
if (Array.isArray(a) !== Array.isArray(b)) return true;
if (a.length !== b.length) return true;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return true;
}
return false;
}
$effect(() => {
if (value !== undefined && node?.props?.[id] !== value) {
node.props = { ...node.props, [id]: value };
const a = $state.snapshot(value);
const b = $state.snapshot(node?.props?.[id]);
const isDiff = Array.isArray(a) ? diffArray(a, b) : a !== b;
if (value !== undefined && isDiff) {
node.props = { ...node.props, [id]: a };
if (graph) {
graph.save();
graph.execute();

View File

@@ -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,17 +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 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();
@@ -30,28 +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: 7,
height: 20,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio
})
);
const pathDisabled = $derived(
createNodePath({
depth: 6,
height: 18,
height: 2000 / height,
y: 50.5,
cornerBottom,
leftBump,
@@ -60,41 +52,49 @@
);
const pathHover = $derived(
createNodePath({
depth: 8,
height: 25,
depth: 7,
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
class="wrapper"
data-node-type={node.type}
data-node-input={id}
class:disabled={!graphState?.possibleSocketIds.has(socketId)}
style:height="{height}px"
style:--socket-color={hoverColor}
class:possible-socket={graphState?.possibleSocketIds.has(socketId)}
>
{#key id && graphId}
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#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}
</div>
{#if node?.state?.type?.inputs?.[id]?.internal !== true}
<div data-node-socket class="large target"></div>
<div
data-node-socket
class="small target"
class="target"
onmousedown={handleMouseDown}
role="button"
tabindex="0"
@@ -106,14 +106,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
width="100"
height="100"
preserveAspectRatio="none"
style={`
--path: path("${path}");
--hover-path: path("${pathHover}");
--hover-path-disabled: path("${pathDisabled}");
`}
style:--path={`path("${path}")`}
style:--hover-path={`path("${pathHover}")`}
>
<path vector-effect="non-scaling-stroke"></path>
</svg>
@@ -123,42 +118,43 @@
.wrapper {
position: relative;
width: 100%;
height: 100px;
transform: translateY(-0.5px);
}
.target {
width: 30px;
height: 30px;
position: absolute;
border-radius: 50%;
top: 50%;
transform: translateY(-50%) translateX(-50%);
/* background: red; */
/* opacity: 0.1; */
}
.small.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.5;
z-index: -10;
}
.large.target {
width: 60px;
height: 60px;
cursor: unset;
pointer-events: none;
}
:global(.hovering-sockets) .large.target {
pointer-events: all;
.target:hover ~ svg path{
d: var(--hover-path);
}
.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;
}
@@ -175,23 +171,20 @@
svg path {
transition: d 0.3s ease, fill 0.3s ease;
fill: var(--layer-1);
fill: var(--color-layer-1);
stroke: var(--stroke);
stroke-width: var(--stroke-width);
d: var(--path);
}
:global {
.hovering-sockets .large:hover ~ svg path {
d: var(--hover-path);
}
stroke-linejoin: round;
shape-rendering: geometricPrecision;
}
.content.disabled {
opacity: 0.2;
}
.disabled svg path {
d: var(--hover-path-disabled) !important;
.possible-socket svg path {
d: var(--hover-path);
}
</style>

View File

@@ -0,0 +1,86 @@
import type { NodeDefinition, NodeId, NodeRegistry } from '@nodarium/types';
export function createMockNodeRegistry(nodes: NodeDefinition[]): NodeRegistry {
const nodesMap = new Map(nodes.map(n => [n.id, n]));
return {
status: 'ready' as const,
load: async (nodeIds: NodeId[]) => {
const loaded: NodeDefinition[] = [];
for (const id of nodeIds) {
if (nodesMap.has(id)) {
loaded.push(nodesMap.get(id)!);
}
}
return loaded;
},
getNode: (id: string) => nodesMap.get(id as NodeId),
getAllNodes: () => Array.from(nodesMap.values()),
register: async () => {
throw new Error('Not implemented in mock');
}
};
}
export const mockFloatOutputNode: NodeDefinition = {
id: 'test/node/output',
inputs: {},
outputs: ['float'],
meta: { title: 'Float Output' },
execute: () => new Int32Array()
};
export const mockFloatInputNode: NodeDefinition = {
id: 'test/node/input',
inputs: { value: { type: 'float' } },
outputs: [],
meta: { title: 'Float Input' },
execute: () => new Int32Array()
};
export const mockGeometryOutputNode: NodeDefinition = {
id: 'test/node/geometry',
inputs: {},
outputs: ['geometry'],
meta: { title: 'Geometry Output' },
execute: () => new Int32Array()
};
export const mockPathInputNode: NodeDefinition = {
id: 'test/node/path',
inputs: { input: { type: 'path', accepts: ['geometry'] } },
outputs: [],
meta: { title: 'Path Input' },
execute: () => new Int32Array()
};
export const mockVec3OutputNode: NodeDefinition = {
id: 'test/node/vec3',
inputs: {},
outputs: ['vec3'],
meta: { title: 'Vec3 Output' },
execute: () => new Int32Array()
};
export const mockIntegerInputNode: NodeDefinition = {
id: 'test/node/integer',
inputs: { value: { type: 'integer' } },
outputs: [],
meta: { title: 'Integer Input' },
execute: () => new Int32Array()
};
export const mockBooleanOutputNode: NodeDefinition = {
id: 'test/node/boolean',
inputs: {},
outputs: ['boolean'],
meta: { title: 'Boolean Output' },
execute: () => new Int32Array()
};
export const mockBooleanInputNode: NodeDefinition = {
id: 'test/node/boolean-input',
inputs: { value: { type: 'boolean' } },
outputs: [],
meta: { title: 'Boolean Input' },
execute: () => new Int32Array()
};

View File

@@ -0,0 +1,110 @@
import { grid } from '$lib/graph-templates/grid';
import { tree } from '$lib/graph-templates/tree';
import { describe, expect, it } from 'vitest';
describe('graph-templates', () => {
describe('grid', () => {
it('should create a grid graph with nodes and edges', () => {
const result = grid(2, 3);
expect(result.nodes.length).toBeGreaterThan(0);
expect(result.edges.length).toBeGreaterThan(0);
});
it('should have output node at the end', () => {
const result = grid(1, 1);
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
expect(outputNode).toBeDefined();
});
it('should create nodes based on grid dimensions', () => {
const result = grid(2, 2);
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
expect(mathNodes.length).toBeGreaterThan(0);
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
expect(outputNode).toBeDefined();
});
it('should have output node at the end', () => {
const result = grid(1, 1);
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
expect(outputNode).toBeDefined();
});
it('should create nodes based on grid dimensions', () => {
const result = grid(2, 2);
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
expect(mathNodes.length).toBeGreaterThan(0);
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
expect(outputNode).toBeDefined();
});
it('should have valid node positions', () => {
const result = grid(3, 2);
result.nodes.forEach(node => {
expect(node.position).toHaveLength(2);
expect(typeof node.position[0]).toBe('number');
expect(typeof node.position[1]).toBe('number');
});
});
it('should generate valid graph structure', () => {
const result = grid(2, 2);
result.nodes.forEach(node => {
expect(typeof node.id).toBe('number');
expect(node.type).toBeTruthy();
});
result.edges.forEach(edge => {
expect(edge).toHaveLength(4);
});
});
});
describe('tree', () => {
it('should create a tree graph with specified depth', () => {
const result = tree(0);
expect(result.nodes.length).toBeGreaterThan(0);
expect(result.edges.length).toBeGreaterThan(0);
});
it('should have root output node', () => {
const result = tree(2);
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
expect(outputNode).toBeDefined();
expect(outputNode?.id).toBe(0);
});
it('should increase node count with depth', () => {
const tree0 = tree(0);
const tree1 = tree(1);
const tree2 = tree(2);
expect(tree0.nodes.length).toBeLessThan(tree1.nodes.length);
expect(tree1.nodes.length).toBeLessThan(tree2.nodes.length);
});
it('should create binary tree structure', () => {
const result = tree(2);
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
expect(mathNodes.length).toBeGreaterThan(0);
const edgeCount = result.edges.length;
expect(edgeCount).toBe(result.nodes.length - 1);
});
it('should have valid node positions', () => {
const result = tree(3);
result.nodes.forEach(node => {
expect(node.position).toHaveLength(2);
expect(typeof node.position[0]).toBe('number');
expect(typeof node.position[1]).toBe('number');
});
});
});
});

View File

@@ -4,4 +4,5 @@ export { default as lottaFaces } from './lotta-faces.json';
export { default as lottaNodesAndFaces } from './lotta-nodes-and-faces.json';
export { default as lottaNodes } from './lotta-nodes.json';
export { plant } from './plant';
export { default as simple } from './simple.json';
export { tree } from './tree';

View File

@@ -0,0 +1,63 @@
{
"id": 0,
"settings": {
"resolution.circle": 54,
"resolution.curve": 20,
"randomSeed": true
},
"meta": {
"title": "New Project",
"lastModified": "2026-02-03T16:56:40.375Z"
},
"nodes": [
{
"id": 9,
"position": [
215,
85
],
"type": "max/plantarium/output",
"props": {}
},
{
"id": 10,
"position": [
165,
72.5
],
"type": "max/plantarium/stem",
"props": {
"amount": 50,
"length": 4,
"thickness": 1
}
},
{
"id": 11,
"position": [
190,
77.5
],
"type": "max/plantarium/noise",
"props": {
"plant": 0,
"scale": 0.5,
"strength": 5
}
}
],
"edges": [
[
10,
0,
11,
"plant"
],
[
11,
0,
9,
"input"
]
]
}

View File

@@ -62,7 +62,7 @@
cursor: ew-resize;
height: 100%;
width: 1px;
background: var(--outline);
background: var(--color-outline);
}
.seperator::before {
content: "";

145
app/src/lib/helpers.test.ts Normal file
View File

@@ -0,0 +1,145 @@
import { clone, debounce, humanizeDuration, humanizeNumber, lerp, snapToGrid } from '$lib/helpers';
import { describe, expect, it } from 'vitest';
describe('helpers', () => {
describe('snapToGrid', () => {
it('should snap to nearest grid point', () => {
expect(snapToGrid(5, 10)).toBe(10);
expect(snapToGrid(15, 10)).toBe(20);
expect(snapToGrid(0, 10)).toBe(0);
expect(snapToGrid(-10, 10)).toBe(-10);
});
it('should snap exact midpoint values', () => {
expect(snapToGrid(5, 10)).toBe(10);
});
it('should use default grid size of 10', () => {
expect(snapToGrid(5)).toBe(10);
expect(snapToGrid(15)).toBe(20);
});
it('should handle values exactly on grid', () => {
expect(snapToGrid(10, 10)).toBe(10);
expect(snapToGrid(20, 10)).toBe(20);
});
});
describe('lerp', () => {
it('should linearly interpolate between two values', () => {
expect(lerp(0, 100, 0)).toBe(0);
expect(lerp(0, 100, 0.5)).toBe(50);
expect(lerp(0, 100, 1)).toBe(100);
});
it('should handle negative values', () => {
expect(lerp(-50, 50, 0.5)).toBe(0);
expect(lerp(-100, 0, 0.5)).toBe(-50);
});
it('should handle t values outside 0-1 range', () => {
expect(lerp(0, 100, -0.5)).toBe(-50);
expect(lerp(0, 100, 1.5)).toBe(150);
});
});
describe('humanizeNumber', () => {
it('should return unchanged numbers below 1000', () => {
expect(humanizeNumber(0)).toBe('0');
expect(humanizeNumber(999)).toBe('999');
});
it('should add K suffix for thousands', () => {
expect(humanizeNumber(1000)).toBe('1K');
expect(humanizeNumber(1500)).toBe('1.5K');
expect(humanizeNumber(999999)).toBe('1000K');
});
it('should add M suffix for millions', () => {
expect(humanizeNumber(1000000)).toBe('1M');
expect(humanizeNumber(2500000)).toBe('2.5M');
});
it('should add B suffix for billions', () => {
expect(humanizeNumber(1000000000)).toBe('1B');
});
});
describe('humanizeDuration', () => {
it('should return ms for very short durations', () => {
expect(humanizeDuration(100)).toBe('100ms');
expect(humanizeDuration(999)).toBe('999ms');
});
it('should format seconds', () => {
expect(humanizeDuration(1000)).toBe('1s');
expect(humanizeDuration(1500)).toBe('1s500ms');
expect(humanizeDuration(59000)).toBe('59s');
});
it('should format minutes', () => {
expect(humanizeDuration(60000)).toBe('1m');
expect(humanizeDuration(90000)).toBe('1m 30s');
});
it('should format hours', () => {
expect(humanizeDuration(3600000)).toBe('1h');
expect(humanizeDuration(3661000)).toBe('1h 1m 1s');
});
it('should format days', () => {
expect(humanizeDuration(86400000)).toBe('1d');
expect(humanizeDuration(90061000)).toBe('1d 1h 1m 1s');
});
it('should handle zero', () => {
expect(humanizeDuration(0)).toBe('0ms');
});
});
describe('debounce', () => {
it('should return a function', () => {
const fn = debounce(() => {}, 100);
expect(typeof fn).toBe('function');
});
it('should only call once when invoked multiple times within delay', () => {
let callCount = 0;
const fn = debounce(() => {
callCount++;
}, 100);
fn();
const firstCall = callCount;
fn();
fn();
expect(callCount).toBe(firstCall);
});
});
describe('clone', () => {
it('should deep clone objects', () => {
const original = { a: 1, b: { c: 2 } };
const cloned = clone(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
expect(cloned.b).not.toBe(original.b);
});
it('should handle arrays', () => {
const original = [1, 2, [3, 4]];
const cloned = clone(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
expect(cloned[2]).not.toBe(original[2]);
});
it('should handle primitives', () => {
expect(clone(42)).toBe(42);
expect(clone('hello')).toBe('hello');
expect(clone(true)).toBe(true);
expect(clone(null)).toBe(null);
});
});
});

View File

@@ -0,0 +1,72 @@
import { isObject, mergeDeep } from '$lib/helpers/deepMerge';
import { describe, expect, it } from 'vitest';
describe('deepMerge', () => {
describe('isObject', () => {
it('should return true for plain objects', () => {
expect(isObject({})).toBe(true);
expect(isObject({ a: 1 })).toBe(true);
});
it('should return false for non-objects', () => {
expect(isObject([])).toBe(false);
expect(isObject('string')).toBe(false);
expect(isObject(42)).toBe(false);
expect(isObject(undefined)).toBe(false);
});
});
describe('mergeDeep', () => {
it('should merge two flat objects', () => {
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = mergeDeep(target, source);
expect(result).toEqual({ a: 1, b: 3, c: 4 });
});
it('should deeply merge nested objects', () => {
const target = { a: { x: 1 }, b: { y: 2 } };
const source = { a: { y: 2 }, c: { z: 3 } };
const result = mergeDeep(target, source);
expect(result).toEqual({
a: { x: 1, y: 2 },
b: { y: 2 },
c: { z: 3 }
});
});
it('should handle multiple sources', () => {
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
const result = mergeDeep(target, source1, source2);
expect(result).toEqual({ a: 1, b: 2, c: 3 });
});
it('should return target if no sources provided', () => {
const target = { a: 1 };
const result = mergeDeep(target);
expect(result).toBe(target);
});
it('should overwrite non-object values', () => {
const target = { a: { b: 1 } };
const source = { a: 'string' };
const result = mergeDeep(target, source);
expect(result.a).toBe('string');
});
it('should handle arrays by replacing', () => {
const target = { a: [1, 2] };
const source = { a: [3, 4] };
const result = mergeDeep(target, source);
expect(result.a).toEqual([3, 4]);
});
});
});

View File

@@ -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(() => {

View File

@@ -0,0 +1,11 @@
export const debugNode = {
id: 'max/plantarium/debug',
inputs: {
input: {
type: '*'
}
},
execute(_data: Int32Array): Int32Array {
return _data;
}
} as const;

View File

@@ -15,8 +15,15 @@ export class RemoteNodeRegistry implements NodeRegistry {
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}`;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Select } from '@nodarium/ui';
import { InputSelect } from '@nodarium/ui';
let activeStore = $state(0);
let { activeId }: { activeId: string } = $props();
@@ -10,7 +10,7 @@
<div class="breadcrumbs">
{#if activeUser}
<Select id="root" options={['root']} bind:value={activeStore}></Select>
<InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
{#if activeCollection}
<button
onclick={() => {
@@ -35,7 +35,7 @@
<span>{activeUser}</span>
{/if}
{:else}
<Select id="root" options={['root']} bind:value={activeStore}></Select>
<InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
{/if}
</div>
@@ -47,7 +47,7 @@
gap: 0.8em;
height: 35px;
box-sizing: border-box;
border-bottom: solid thin var(--outline);
border-bottom: solid thin var(--color-outline);
}
.breadcrumbs > button {
position: relative;

View File

@@ -51,7 +51,9 @@
tabindex="0"
ondragstart={handleDragStart}
>
<NodeHtml bind:node={nodeData} inView={true} position="relative" z={5} />
{#if nodeData}
<NodeHtml bind:node={nodeData} inView={true} position="relative" z={5} />
{/if}
</div>
</div>
@@ -66,7 +68,7 @@
}
.dragging {
border: dashed 2px var(--outline);
border: dashed 2px var(--color-outline);
}
.node-wrapper > div {
opacity: 1;

View File

@@ -74,7 +74,7 @@
.wrapper {
position: relative;
border-bottom: solid thin var(--outline);
border-bottom: solid thin var(--color-outline);
display: flex;
}
p {
@@ -86,15 +86,15 @@
position: absolute;
}
svg {
height: 124px;
height: 126px;
margin: 24px 0px;
border-top: solid thin var(--outline);
border-bottom: solid thin var(--outline);
border-top: solid thin var(--color-outline);
border-bottom: solid thin var(--color-outline);
width: 100%;
}
polyline {
fill: none;
stroke: var(--layer-3);
stroke: var(--color-layer-3);
opacity: 0.5;
stroke-width: 1;
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { humanizeNumber } from '$lib/helpers';
import { Checkbox } from '@nodarium/ui';
import { InputCheckbox } from '@nodarium/ui';
import type { PerformanceData } from '@nodarium/utils';
import BarSplit from './BarSplit.svelte';
import Monitor from './Monitor.svelte';
@@ -195,7 +195,7 @@
<div class="p-4 performance-tabler">
<div class="flex items-center gap-2">
<Checkbox id="show-total" bind:value={showAverage} />
<InputCheckbox id="show-total" bind:value={showAverage} />
<label for="show-total">Show Average</label>
</div>

View File

@@ -25,7 +25,7 @@
}
polyline {
fill: none;
stroke: var(--layer-3);
stroke: var(--color-layer-3);
opacity: 1;
stroke-width: 1;
}

View File

@@ -74,14 +74,14 @@
top: 10px;
left: 10px;
z-index: 2;
background: var(--layer-0);
border: solid thin var(--outline);
background: var(--color-layer-0);
border: solid thin var(--color-outline);
border-collapse: collapse;
}
td {
padding: 4px;
padding-inline: 8px;
font-size: 0.8em;
border: solid thin var(--outline);
border: solid thin var(--color-outline);
}
</style>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { defaultPlant, lottaFaces, plant } from '$lib/graph-templates';
import { defaultPlant, lottaFaces, plant, simple } from '$lib/graph-templates';
import type { Graph } from '$lib/types';
import { InputSelect } from '@nodarium/ui';
import type { ProjectManager } from './project-manager.svelte';
const { projectManager } = $props<{ projectManager: ProjectManager }>();
let showNewProject = $state(false);
let newProjectName = $state('');
let selectedTemplate = $state('defaultPlant');
const templates = [
{
@@ -16,25 +16,27 @@
graph: defaultPlant as unknown as Graph
},
{ name: 'Plant', value: 'plant', graph: plant as unknown as Graph },
{ name: 'Simple', value: 'simple', graph: simple as unknown as Graph },
{
name: 'Lotta Faces',
value: 'lottaFaces',
graph: lottaFaces as unknown as Graph
}
];
let selectedTemplateIndex = $state(0);
function handleCreate() {
const template = templates.find((t) => t.value === selectedTemplate) || templates[0];
const template = templates[selectedTemplateIndex] || templates[0];
projectManager.handleCreateProject(template.graph, newProjectName);
newProjectName = '';
showNewProject = false;
}
</script>
<header class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center">
<header class="flex justify-between px-4 h-[70px] border-b-1 border-outline items-center bg-layer-2">
<h3>Project</h3>
<button
class="px-3 py-1 bg-[var(--layer-0)] rounded"
class="px-3 py-1 bg-layer-1 rounded"
onclick={() => (showNewProject = !showNewProject)}
>
New
@@ -42,24 +44,17 @@
</header>
{#if showNewProject}
<div class="flex flex-col px-4 py-3 border-b-1 border-[var(--outline)] gap-2">
<div class="flex flex-col px-4 py-3.5 mt-[1px] border-b-1 border-outline gap-3">
<input
type="text"
bind:value={newProjectName}
placeholder="Project name"
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
class="w-full px-2 py-2 bg-layer-2 rounded"
onkeydown={(e) => e.key === 'Enter' && handleCreate()}
/>
<select
bind:value={selectedTemplate}
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
>
{#each templates as template (template.name)}
<option value={template.value}>{template.name}</option>
{/each}
</select>
<InputSelect options={templates.map(t => t.name)} bind:value={selectedTemplateIndex} />
<button
class="cursor-pointer self-end px-3 py-1 bg-blue-600 rounded"
class="cursor-pointer self-end px-3 py-1 bg-selected rounded"
onclick={() => handleCreate()}
>
Create
@@ -67,20 +62,22 @@
</div>
{/if}
<div class="p-4 text-white min-h-screen">
<div class="text-white min-h-screen">
{#if projectManager.loading}
<p>Loading...</p>
{/if}
<ul class="space-y-2">
<ul>
{#each projectManager.projects as project (project.id)}
<li>
<div
class="
w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
h-[70px] border-b-1 border-b-outline
flex
w-full text-left px-3 py-2 cursor-pointer {projectManager
.activeProjectId.value === project.id
? 'bg-blue-600'
: 'bg-gray-800 hover:bg-gray-700'}
? 'border-l-2 border-l-selected pl-2.5!'
: ''}
"
onclick={() => projectManager.handleSelectProject(project.id!)}
role="button"
@@ -89,10 +86,10 @@
e.key === 'Enter'
&& projectManager.handleSelectProject(project.id!)}
>
<div class="flex justify-between items-center">
<div class="flex justify-between items-center grow">
<span>{project.meta?.title || 'Untitled'}</span>
<button
class="text-red-400 hover:text-red-300"
class="text-layer-1! bg-red-500 w-7 text-xl rounded-sm cursor-pointer opacity-20 hover:opacity-80"
onclick={() => {
projectManager.handleDeleteProject(project.id!);
}}

View File

@@ -54,7 +54,7 @@ export class ProjectManager {
g.id = id;
if (!g.meta) g.meta = {};
if (!g.meta.title) g.meta.title = title;
g.meta.title = title;
db.saveGraph(g);
this.projects = [...this.projects, g];

View 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} />

View File

@@ -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>

View File

@@ -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 { Vector3 } from 'three';
import { DoubleSide } from 'three';
import { type Group, MeshMatcapMaterial, TextureLoader } from 'three';
import { createGeometryPool, createInstancedGeometryPool } from './geometryPool';
import Scene from './Scene.svelte';
@@ -14,7 +14,8 @@
matcap.colorSpace = 'srgb';
const material = new MeshMatcapMaterial({
color: 0xffffff,
matcap
matcap,
side: DoubleSide
});
let sceneComponent = $state<ReturnType<typeof Scene>>();
@@ -22,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);
@@ -39,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);
@@ -88,7 +62,7 @@
};
</script>
{#if appSettings.value.debug.showPerformancePanel}
{#if appSettings.value.debug.advancedMode}
<SmallPerformanceViewer {fps} store={perf} />
{/if}
@@ -96,8 +70,8 @@
<Canvas>
<Scene
bind:this={sceneComponent}
{lines}
{centerCamera}
{debugData}
bind:scene
bind:fps
/>

View 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();
}

View File

@@ -28,7 +28,7 @@ function getValue(input: NodeInput, value?: unknown) {
}
if (Array.isArray(value)) {
if (input.type === 'vec3') {
if (input.type === 'vec3' || input.type === 'shape') {
return [
0,
value.length + 1,
@@ -59,6 +59,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map();
private seed = Math.floor(Math.random() * 100000000);
private debugData: Record<number, { type: string; data: Int32Array }> = {};
perf?: PerformanceStore;
@@ -124,10 +125,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
}
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;
@@ -136,16 +137,31 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
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');
let a = performance.now();
this.debugData = {};
// Then we add some metadata to the graph
const [outputNode, nodes] = await this.addMetaData(graph);
@@ -237,6 +253,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,6 +267,12 @@ 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();
@@ -273,6 +301,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
return res as unknown as Int32Array;
}
getDebugData() {
return this.debugData;
}
getPerformanceData() {
return this.perf?.get();
}

View File

@@ -5,6 +5,7 @@ type RuntimeState = {
parents: RuntimeNode[];
children: RuntimeNode[];
inputNodes: Record<string, RuntimeNode>;
debugNode?: boolean;
};
export type RuntimeNode = SerializedNode & { state: RuntimeState };

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -56,6 +56,10 @@
return 0;
}
if (Array.isArray(inputValue) && node.type === 'vec3') {
return inputValue;
}
// If the component is supplied with a default value use that
if (inputValue !== undefined && typeof inputValue !== 'object') {
return inputValue;
@@ -98,7 +102,7 @@
&& typeof internalValue === 'number'
) {
value[key] = node?.options?.[internalValue];
} else if (internalValue) {
} else if (internalValue !== undefined) {
value[key] = internalValue;
}
});
@@ -124,7 +128,6 @@
{#if key && isNodeInput(type?.[key])}
{@const inputType = type[key]}
<!-- Leaf input -->
<div class="input input-{inputType.type}" class:first-level={depth === 1}>
{#if inputType.type === 'button'}
<button onclick={handleClick}>
@@ -138,7 +141,6 @@
{/if}
</div>
{:else if depth === 0}
<!-- Root: iterate over top-level keys -->
{#each Object.keys(type ?? {}).filter((k) => k !== 'title') as childKey (childKey)}
<NestedSettings
id={`${id}.${childKey}`}
@@ -150,7 +152,6 @@
{/each}
<hr />
{:else if key && type?.[key]}
<!-- Group -->
{#if depth > 0}
<hr />
{/if}
@@ -210,7 +211,12 @@
.first-level.input {
padding-left: 1em;
padding-right: 1em;
padding-bottom: 1px;
padding-bottom: 0.5px;
gap: 3px;
}
.first-level.input-boolean {
gap: 10px;
}
button {
@@ -218,11 +224,10 @@
}
hr {
position: absolute;
margin: 0;
left: 0;
right: 0;
border: none;
border-bottom: solid thin var(--outline);
border-bottom: solid thin var(--color-outline);
}
</style>

View File

@@ -6,6 +6,7 @@ const themes = [
'catppuccin',
'solarized',
'high-contrast',
'high-contrast-light',
'nord',
'dracula'
] as const;
@@ -29,10 +30,11 @@ export const AppSettingTypes = {
},
nodeInterface: {
title: 'Node Interface',
showNodeGrid: {
type: 'boolean',
label: 'Show Grid',
value: true
backgroundType: {
type: 'select',
label: 'Background',
options: ['grid', 'dots', 'none'],
value: 'grid'
},
snapToGrid: {
type: 'boolean',
@@ -57,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: {

View File

@@ -33,7 +33,7 @@
{#if panelState.activePanel.value === id}
<div class="wrapper" class:hidden>
{#if title}
<header>
<header class="bg-layer-2">
<h3>{title}</h3>
</header>
{/if}
@@ -43,7 +43,7 @@
<style>
header {
border-bottom: solid thin var(--outline);
border-bottom: solid thin var(--color-outline);
height: 70px;
display: flex;
align-items: center;

View File

@@ -2,13 +2,16 @@
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}>
<div class="tabs">
<button aria-label="Close" onclick={() => state.toggleOpen()}>
<span class="icon-[tabler--settings]"></span>
<span class="absolute i-[tabler--chevron-left] w-6 h-6 block"></span>
</button>
{#each state.keys as panelId (panelId)}
@@ -45,7 +48,7 @@
}
.content {
background: var(--layer-1);
background: var(--color-layer-1);
z-index: 10;
position: relative;
max-height: 100vh;
@@ -55,7 +58,7 @@
.tabs {
display: flex;
flex-direction: column;
border-right: solid thin var(--outline);
border-right: solid thin var(--color-outline);
}
.tabs > button {
@@ -66,9 +69,9 @@
border: none;
display: flex;
align-items: center;
border-bottom: solid thin var(--outline);
border-left: solid thin var(--outline);
background: var(--layer-1);
border-bottom: solid thin var(--color-outline);
border-left: solid thin var(--color-outline);
background: var(--color-layer-1);
}
.tabs > button > span {
@@ -76,7 +79,7 @@
}
.tabs > button.active {
background: var(--layer-2);
background: var(--color-layer-2);
}
.tabs > button.active span {

View File

@@ -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 });
}
}
});
@@ -95,5 +97,5 @@
type={nodeDefinition}
/>
{:else}
<p class="mx-4">Node has no settings</p>
<p class="mx-4 mt-4">Node has no settings</p>
{/if}

View File

@@ -11,14 +11,16 @@
let { manager, node = $bindable() }: Props = $props();
</script>
<div class='{node?"border-l-2 pl-3.5!":""} bg-layer-2 flex items-center h-[70px] border-b-1 border-l-selected border-b-outline pl-4'>
<h3>Node Settings</h3>
</div>
{#if node}
{#key node.id}
{#if node}
<ActiveNodeSelected {manager} bind:node />
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No node selected</p>
<p class="mx-4 mt-4">No node selected</p>
{/if}

View File

@@ -8,7 +8,7 @@
import { humanizeDuration } from '$lib/helpers';
import { localState } from '$lib/helpers/localState.svelte';
import Monitor from '$lib/performance/Monitor.svelte';
import { Float } from '@nodarium/ui';
import { InputNumber } from '@nodarium/ui';
import { writable } from 'svelte/store';
function calculateStandardDeviation(array: number[]) {
@@ -125,7 +125,7 @@
</progress>
{:else}
<label for="bench-samples">Samples</label>
<Float id="bench-sample" bind:value={amount.value} max={1000} />
<InputNumber id="bench-sample" bind:value={amount.value} max={1000} step={1} />
<button onclick={benchmark} disabled={isRunning}>start</button>
{/if}
</div>
@@ -138,7 +138,7 @@
gap: 1em;
}
.monitor-wrapper {
border: solid thin var(--outline);
border: solid thin var(--color-outline);
border-bottom: none;
}
i {

View File

@@ -0,0 +1,185 @@
<script lang="ts">
import { Details } from '@nodarium/ui';
import { micromark } from 'micromark';
type Props = {
git?: Record<string, string>;
changelog?: string;
};
const {
git,
changelog
}: Props = $props();
const typeMap = new Map([
['fix', 'border-l-red-800'],
['feat', 'border-l-green-800'],
['chore', 'border-l-gray-800'],
['docs', 'border-l-blue-800'],
['refactor', 'border-l-purple-800'],
['ci', 'border-l-red-400']
]);
function detectCommitType(commit: string) {
for (const key of typeMap.keys()) {
if (commit.startsWith(key)) {
return key;
}
}
return '';
}
function parseCommit(line?: string) {
if (!line) return;
const regex = /^\s*-\s*\[([a-f0-9]+)\]\((https?:\/\/[^\s)]+)\)\s+(.+)$/;
const match = line.match(regex);
if (!match) {
return;
}
const [, sha, link, description] = match;
return {
sha,
link,
description,
type: detectCommitType(description)
};
}
function parseChangelog(md: string) {
return md.split(/^# v/gm)
.filter(l => !!l.length)
.map(release => {
const [firstLine, ...rest] = release.split('\n');
const title = firstLine.trim();
const blocks = rest
.join('\n')
.split('---');
const commits = blocks.length > 1
? blocks
.at(-1)
?.split('\n')
?.map(line => parseCommit(line))
?.filter(c => !!c)
: [];
const description = (
blocks.length > 1
? blocks
.slice(0, -1)
.join('\n')
: blocks[0]
).trim();
return {
description: micromark(description),
title,
commits
};
});
}
</script>
<div id="changelog" class="p-4 font-mono text-text overflow-y-auto max-h-full space-y-5">
{#if git}
<div class="mb-4 p-3 bg-layer-2 text-xs rounded">
<p><strong>Branch:</strong> {git.branch}</p>
<p>
<strong>Commit:</strong>
<a
href="https://git.max-richter.dev/max/nodarium/commit/{git.sha}"
class="link"
target="_blank"
>
{git.sha.slice(0, 7)}
</a>
{git.commit_message}
</p>
<p>
<strong>Commits since last release:</strong>
{git.commits_since_last_release}
</p>
<p>
<strong>Timestamp:</strong>
{new Date(git.commit_timestamp).toLocaleString()}
</p>
</div>
{/if}
{#if changelog}
{#each parseChangelog(changelog) as release (release)}
<Details title={release.title}>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
<div id="description" class="pb-5">{@html release.description}</div>
{#if release?.commits?.length}
<Details
title="All Commits"
class="commits"
>
{#each release.commits as commit (commit)}
<p class="py-1 leading-7 text-xs border-b-1 border-l-1 border-b-outline last:border-b-0 -ml-2 pl-2 {typeMap.get(commit.type)}">
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={commit.link} class="link" target="_blank">{commit.sha}</a>
{commit.description}
</p>
{/each}
</Details>
{/if}
</Details>
{/each}
{/if}
</div>
<style lang="postcss">
@reference "tailwindcss";
#changelog :global(.commits) {
margin-left: -16px;
margin-right: -16px;
border-radius: 0px 0px 2px 2px !important;
}
#changelog :global(details > div){
padding-bottom: 0px;
}
#changelog :global(.commits > div) {
padding-bottom: 0px;
padding-top: 0px;
}
#description :global(h2) {
@apply font-bold mt-4 mb-1;
}
#description :global(h2:first-child) {
margin-top: 0px !important;
}
#description :global(ul) {
padding-left: 1em;
}
#description :global(li),
#description :global(p) {
@apply text-xs!;
list-style-type: disc;
}
#changelog :global(details > details[open] > summary){
margin-bottom: 20px !important;
}
.link {
color: #60a5fa;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
</style>

View File

@@ -7,7 +7,7 @@
return JSON.stringify(
{
...g,
nodes: g.nodes.map((n: object) => ({ ...n, tmp: undefined }))
nodes: g.nodes.map((n: object) => ({ ...n, tmp: undefined, state: undefined }))
},
null,
2

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { page } from '$app/state';
</script>
<main class="w-screen h-screen flex flex-col items-center justify-center">
<div class="outline-1 outline-outline bg-layer-2">
<h1 class="p-8 text-3xl">@nodarium/error</h1>
<hr>
<pre class="p-8">{JSON.stringify(page.error, null, 2)}</pre>
<hr>
<div class="flex p-4">
<button
class="bg-layer-2 outline-1 outline-outline p-3 px-6 rounded-sm cursor-pointer"
on:click={() => window.location.reload()}
>
reload
</button>
</div>
</div>
</main>

View File

@@ -1 +1,28 @@
export const prerender = true;
export async function load({ fetch }) {
async function fetchChangelog() {
try {
const res = await fetch('/CHANGELOG.md');
return await res.text();
} catch (error) {
console.log('Failed to fetch CHANGELOG.md', error);
return;
}
}
async function fetchGitInfo() {
try {
const res = await fetch('/git.json');
return await res.json();
} catch (error) {
console.log('Failed to fetch git.json', error);
return;
}
}
return {
git: await fetchGitInfo(),
changelog: await fetchChangelog()
};
}

View File

@@ -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';
@@ -17,6 +18,7 @@
import Panel from '$lib/sidebar/Panel.svelte';
import ActiveNodeSettings from '$lib/sidebar/panels/ActiveNodeSettings.svelte';
import BenchmarkPanel from '$lib/sidebar/panels/BenchmarkPanel.svelte';
import Changelog from '$lib/sidebar/panels/Changelog.svelte';
import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte';
import GraphSource from '$lib/sidebar/panels/GraphSource.svelte';
import Keymap from '$lib/sidebar/panels/Keymap.svelte';
@@ -28,8 +30,11 @@
let performanceStore = createPerformanceStore();
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);
@@ -60,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() {
@@ -101,6 +108,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;
@@ -159,6 +167,7 @@
bind:scene
bind:this={viewerComponent}
perf={performanceStore}
debugData={debugData}
centerCamera={appSettings.value.centerCamera}
/>
</Grid.Cell>
@@ -168,7 +177,8 @@
graph={pm.graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
addMenuPadding={{ right: sidebarOpen ? 330 : undefined }}
backgroundType={appSettings.value.nodeInterface.backgroundType}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.value.nodeInterface.showHelp}
@@ -178,7 +188,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"
@@ -211,7 +221,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}
@@ -224,7 +234,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()} />
@@ -232,7 +242,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} />
@@ -247,13 +257,14 @@
type={graphSettingTypes}
bind:value={graphSettings}
/>
<ActiveNodeSettings {manager} bind:node={activeNode} />
</Panel>
<Panel
id="active-node"
title="Node Settings"
icon="i-[tabler--adjustments] bg-blue-400"
id="changelog"
title="Changelog"
icon="i-[tabler--file-text-spark] bg-green-400"
>
<ActiveNodeSettings {manager} bind:node={activeNode} />
<Changelog git={data.git} changelog={data.changelog} />
</Panel>
</Sidebar>
</Grid.Cell>
@@ -262,7 +273,7 @@
<style>
header {
background-color: var(--layer-1);
background-color: var(--color-layer-1);
}
.wrapper {

View File

@@ -7,7 +7,6 @@
import Sidebar from '$lib/sidebar/Sidebar.svelte';
import { type NodeId, type NodeInstance } from '@nodarium/types';
import { concatEncodedArrays, createWasmWrapper, encodeNestedArray } from '@nodarium/utils';
import Code from './Code.svelte';
const registryCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', registryCache);
@@ -78,11 +77,7 @@
</Grid.Cell>
<Grid.Cell>
<div class="h-screen w-[80vw] overflow-y-auto">
{#if nodeWasm}
<Code wasm={nodeWasm} />
{/if}
</div>
<div class="h-screen w-[80vw] overflow-y-auto"></div>
</Grid.Cell>
</Grid.Row>

View File

@@ -1,26 +0,0 @@
<script lang="ts">
import wabtInit from 'wabt';
const { wasm } = $props<{ wasm: ArrayBuffer }>();
async function toWat(arrayBuffer: ArrayBuffer) {
const wabt = await wabtInit();
const module = wabt.readWasm(new Uint8Array(arrayBuffer), {
readDebugNames: true
});
module.generateNames();
module.applyNames();
return module.toText({ foldExprs: false, inlineExport: false });
}
</script>
{#await toWat(wasm)}
<p>Converting to WAT</p>
{:then c}
<pre>
<code class="text-gray-50">{c}</code>
</pre>
{/await}

View File

@@ -1 +1,3 @@
nodes/
CHANGELOG.md
git.json

19
app/static/favicon.svg Normal file
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,9 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import { playwright } from '@vitest/browser-playwright';
import comlink from 'vite-plugin-comlink';
import glsl from 'vite-plugin-glsl';
import wasm from 'vite-plugin-wasm';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
@@ -20,5 +21,36 @@ export default defineConfig({
},
ssr: {
noExternal: ['three']
},
build: {
chunkSizeWarningLimit: 2000
},
test: {
expect: { requireAssertions: true },
projects: [
{
extends: './vite.config.ts',
test: {
name: 'client',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'firefox', headless: true }]
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**']
}
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
}
}
]
}
});

View File

@@ -8,15 +8,16 @@ The visual programming language consists of so called `Nodes` which are stored a
```typescript
type Node = {
id: string,
outputs: string[],
id: string;
outputs: string[];
inputs: {
[key:string]: NodeInput
}
}
[key: string]: NodeInput;
};
};
```
## How are the arguments defined?
To define which arguments a nodes accepts we use JSON. This json is embeded into the `.wasm` file of our node. An example `definition.json` file could look like this:
```json

View File

@@ -5,21 +5,23 @@
Plantarium encodes different types of data differently. Right now we have three types of data:
### Path
[0, stem_depth, ... x,y,z,thickness, x,y,z,thickness...]
### Geometry
[1, vertex_amount, face_amount, ... faces in index_a, index_b, index_c ..., ... vertices in x,y,z,x,y,z ..., ... normals in x,y,z,x,y,z ...]
### Instances
[
2,
vertex_amount,
face_amount,
instance_amount,
stem_depth,
...faces...,
...vertices...,
...normals...,
...4x4 matrices for instances...
]
[
2,
vertex_amount,
face_amount,\
instance_amount,
stem_depth,
...faces...,
...vertices...,
...normals...,
...4x4 matrices for instances...
]

View File

@@ -11,8 +11,8 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"]
[dependencies]
utils = { version = "0.1.0", path = "../../../../packages/utils" }
console_error_panic_hook = { version = "0.1.7", optional = true }
macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
console_error_panic_hook = { version = "0.1.7", optional = true }
utils = { version = "0.1.0", path = "../../../../packages/utils" }
web-sys = { version = "0.3.69", features = ["console"] }

View File

@@ -8,5 +8,5 @@ edition = "2018"
crate-type = ["cdylib", "rlib"]
[dependencies]
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }

View File

@@ -8,5 +8,5 @@ edition = "2018"
crate-type = ["cdylib", "rlib"]
[dependencies]
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }

View File

@@ -8,6 +8,6 @@ edition = "2018"
crate-type = ["cdylib", "rlib"]
[dependencies]
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
glam = "0.30.10"
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }

View File

@@ -8,6 +8,6 @@ edition = "2018"
crate-type = ["cdylib", "rlib"]
[dependencies]
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
glam = "0.30.10"
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }

Some files were not shown because too many files have changed in this diff Show More