Compare commits
31 Commits
812099c55d
...
v0.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
419249aca3 | ||
|
c69cb94ac7
|
|||
|
|
4b652d885f | ||
|
381f784775
|
|||
| 91866b4e9a | |||
|
01f1568221
|
|||
|
3e8d2768b3
|
|||
|
16a832779a
|
|||
|
d582915842
|
|||
|
|
caaecd7a02 | ||
|
93ca436412
|
|||
|
ecdb986a96
|
|||
|
304abf2866
|
|||
|
a547d86946
|
|||
|
667d140883
|
|||
|
0ac65fd7a7
|
|||
|
|
5437e062e1
|
||
|
|
1015d17afb
|
||
|
|
fd8e5e92d2
|
||
| a2c2503a8e | |||
|
|
e18f75e1b8
|
||
|
|
6a178dc3a7
|
||
|
|
76cdfee018
|
||
|
|
b19da950a6
|
||
|
|
89e4cf8364
|
||
|
|
a28f15c256
|
||
|
|
57e3a707c5
|
||
|
|
dced7db3ad
|
||
|
|
c2dc538c05
|
||
|
|
9484b3599e
|
||
|
|
3c168aa9b6
|
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
79
.gitea/scripts/create-release.sh
Executable file
79
.gitea/scripts/create-release.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
TAG="$GITHUB_REF_NAME"
|
||||
VERSION=$(echo "$TAG" | sed 's/^v//')
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
|
||||
echo "🚀 Creating release for $TAG (safe mode)"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 1. Extract release notes from annotated tag
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
NOTES=$(git tag -l "$TAG" --format='%(contents)')
|
||||
|
||||
if [ -z "$NOTES" ]; then
|
||||
echo "❌ Tag message is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout main
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 2. Update all package.json versions
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
echo "🔧 Updating package.json versions to $VERSION"
|
||||
|
||||
find . -name package.json ! -path "*/node_modules/*" | while read file; do
|
||||
tmp_file="$file.tmp"
|
||||
jq --arg v "$VERSION" '.version = $v' "$file" >"$tmp_file"
|
||||
mv "$tmp_file" "$file"
|
||||
done
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 3. Update CHANGELOG.md (prepend)
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
tmp_changelog="CHANGELOG.tmp"
|
||||
{
|
||||
echo "## $TAG ($DATE)"
|
||||
echo ""
|
||||
echo "$NOTES"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
cat CHANGELOG.md
|
||||
fi
|
||||
} >"$tmp_changelog"
|
||||
|
||||
mv "$tmp_changelog" CHANGELOG.md
|
||||
|
||||
pnpm exec dprint fmt CHANGELOG.md
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 4. Create release commit
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
git config user.name "release-bot"
|
||||
git config user.email "release-bot@ci"
|
||||
|
||||
git add CHANGELOG.md $(find . -name package.json ! -path "*/node_modules/*")
|
||||
|
||||
# Skip commit if nothing changed
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit for release $TAG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore(release): $TAG"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# 5. Push changes
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
git push origin main
|
||||
|
||||
echo "✅ Release commit for $TAG created successfully (tag untouched)"
|
||||
@@ -1,114 +0,0 @@
|
||||
name: 🏗️ Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["*"]
|
||||
|
||||
env:
|
||||
PNPM_CACHE_FOLDER: .pnpm-store
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
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') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
- name: 📦 Install Dependencies
|
||||
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
|
||||
|
||||
- name: 🗜️ Tar Workspace
|
||||
run: tar -cf workspace.tar --exclude="${{ env.PNPM_CACHE_FOLDER }}" .
|
||||
|
||||
- name: 📤 Upload Workspace
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: workspace-deps
|
||||
path: workspace.tar
|
||||
compression-level: 0
|
||||
|
||||
lint:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
steps:
|
||||
- name: 📥 Download Workspace
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: workspace-deps
|
||||
- name: 🔓 Untar Workspace
|
||||
run: tar -xf workspace.tar && rm workspace.tar
|
||||
- name: 🧹 Run Linter
|
||||
run: pnpm lint
|
||||
|
||||
format:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
steps:
|
||||
- name: 📥 Download Workspace
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: workspace-deps
|
||||
- name: 🔓 Untar Workspace
|
||||
run: tar -xf workspace.tar && rm workspace.tar
|
||||
- name: 🎨 Check Formatting
|
||||
run: pnpm format:check
|
||||
|
||||
typecheck:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
steps:
|
||||
- name: 📥 Download Workspace
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: workspace-deps
|
||||
- name: 🔓 Untar Workspace
|
||||
run: tar -xf workspace.tar && rm workspace.tar
|
||||
- name: 🧬 Type Check
|
||||
run: pnpm check
|
||||
|
||||
build_and_deploy:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
needs: [lint, format, typecheck]
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
steps:
|
||||
- name: 📥 Download Workspace
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: workspace-deps
|
||||
- name: 🔓 Untar Workspace
|
||||
run: tar -xf workspace.tar && rm workspace.tar
|
||||
- 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 }}
|
||||
84
.gitea/workflows/release.yaml
Normal file
84
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
name: 🚀 Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["*"]
|
||||
tags: ["*"]
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
|
||||
env:
|
||||
PNPM_CACHE_FOLDER: ~/.pnpm-store
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container: jimfx/nodes:latest
|
||||
|
||||
steps:
|
||||
- name: 📑 Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 💾 Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.PNPM_CACHE_FOLDER }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
|
||||
|
||||
- name: 🧹 Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: 🎨 Format Check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: 🧬 Type Check
|
||||
run: pnpm check
|
||||
|
||||
- name: 🛠️ Build
|
||||
run: pnpm build:deploy
|
||||
|
||||
- name: 🔬 Tests
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" pnpm test
|
||||
|
||||
- name: 🚀 Create Release Commit
|
||||
if: github.ref_type == 'tag'
|
||||
run: ./.gitea/scripts/create-release.sh
|
||||
|
||||
- name: 🏷️ Create Gitea Release
|
||||
if: github.ref_type == 'tag'
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
release_name: Release ${{ github.ref_name }}
|
||||
body_path: CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: 🔑 Configure rclone
|
||||
if: github.ref_type == 'tag'
|
||||
run: |
|
||||
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
|
||||
chmod 600 /tmp/id_rsa
|
||||
mkdir -p ~/.config/rclone
|
||||
echo -e "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||
SSH_USER: ${{ vars.SSH_USER }}
|
||||
|
||||
- name: 🚀 Deploy Changed Files via rclone
|
||||
if: github.ref_type == 'tag'
|
||||
run: |
|
||||
echo "Uploading the rest"
|
||||
rclone sync --update -v --progress --exclude _astro/** --stats 2s --stats-one-line ./app/build/ sftp-remote:${REMOTE_DIR} --transfers 4
|
||||
env:
|
||||
REMOTE_DIR: ${{ vars.REMOTE_DIR }}
|
||||
19
CHANGELOG.md
Normal file
19
CHANGELOG.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## v0.0.2 (2026-02-03)
|
||||
|
||||
fix(ci): actually deploy on tags
|
||||
|
||||
---
|
||||
|
||||
## v0.0.2 (2026-02-03)
|
||||
|
||||
fix(app): correctly handle false value in settings
|
||||
|
||||
This caused a bug where random seed could not be false.
|
||||
|
||||
---
|
||||
|
||||
## v0.0.1 (2026-02-03)
|
||||
|
||||
chore: format
|
||||
|
||||
---
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,15 +1,29 @@
|
||||
FROM node:24-alpine
|
||||
# FROM jacoblincool/playwright:chromium-light
|
||||
FROM jacoblincool/playwright:firefox
|
||||
|
||||
RUN apk add --no-cache --update curl rclone g++
|
||||
# RUN apk add --no-cache curl git jq g++ make
|
||||
RUN apt update && apt install -y curl git jq g++ make \
|
||||
libgl1-mesa-dri \
|
||||
libglapi-mesa \
|
||||
libosmesa6 \
|
||||
mesa-utils \
|
||||
xvfb \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set Rust paths
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH
|
||||
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
# Install Rust, wasm target, and pnpm
|
||||
RUN curl --silent --show-error --location --fail --retry 3 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
--output /tmp/rustup-init.sh https://sh.rustup.rs \
|
||||
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
|
||||
&& rm /tmp/rustup-init.sh \
|
||||
&& rustup target add wasm32-unknown-unknown \
|
||||
&& npm i -g pnpm
|
||||
&& rm -rf /usr/local/rustup/toolchains/*/share/doc \
|
||||
&& npm i -g pnpm \
|
||||
&& pnpx playwright install firefox
|
||||
|
||||
25
README.md
25
README.md
@@ -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
2
app/.gitignore
vendored
@@ -27,3 +27,5 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
build/
|
||||
|
||||
test-results/
|
||||
|
||||
62
app/e2e/main.test.ts
Normal file
62
app/e2e/main.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('test', async ({ page }) => {
|
||||
// Listen for console messages
|
||||
page.on('console', msg => {
|
||||
console.log(`[Browser Console] ${msg.type()}: ${msg.text()}`);
|
||||
});
|
||||
|
||||
await page.goto('http://localhost:4173', { waitUntil: 'load' });
|
||||
|
||||
// await expect(page).toHaveScreenshot();
|
||||
await expect(page.locator('.graph-wrapper')).toHaveScreenshot();
|
||||
|
||||
await page.getByRole('button', { name: 'projects' }).click();
|
||||
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||
await page.getByRole('combobox').selectOption('2');
|
||||
await page.getByRole('textbox', { name: 'Project name' }).click();
|
||||
await page.getByRole('textbox', { name: 'Project name' }).fill('Test Project');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
const expectedNodes = [
|
||||
{
|
||||
id: '10',
|
||||
type: 'max/plantarium/stem',
|
||||
props: {
|
||||
amount: 50,
|
||||
length: 4,
|
||||
thickness: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
type: 'max/plantarium/noise',
|
||||
props: {
|
||||
scale: 0.5,
|
||||
strength: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
type: 'max/plantarium/output'
|
||||
}
|
||||
];
|
||||
|
||||
for (const node of expectedNodes) {
|
||||
const wrapper = page.locator(
|
||||
`div.wrapper[data-node-id="${node.id}"][data-node-type="${node.type}"]`
|
||||
);
|
||||
await expect(wrapper).toBeVisible();
|
||||
if ('props' in node) {
|
||||
const props = node.props as unknown as Record<string, number>;
|
||||
for (const propId in node.props) {
|
||||
const expectedValue = props[propId];
|
||||
const inputElement = page.locator(
|
||||
`div.wrapper[data-node-type="${node.type}"][data-node-input="${propId}"] input[type="number"]`
|
||||
);
|
||||
const value = parseFloat(await inputElement.inputValue());
|
||||
expect(value).toBe(expectedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
BIN
app/e2e/main.test.ts-snapshots/test-1-linux.png
Normal file
BIN
app/e2e/main.test.ts-snapshots/test-1-linux.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "@nodarium/app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"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' .",
|
||||
@@ -25,8 +27,7 @@
|
||||
"idb": "^8.0.3",
|
||||
"jsondiffpatch": "^0.7.3",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"three": "^0.182.0",
|
||||
"wabt": "^1.0.39"
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
@@ -34,11 +35,13 @@
|
||||
"@iconify-json/tabler": "^1.2.26",
|
||||
"@iconify/tailwind4": "^1.2.1",
|
||||
"@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",
|
||||
"@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",
|
||||
@@ -52,6 +55,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.17",
|
||||
"vitest-browser-svelte": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
20
app/playwright.config.ts
Normal file
20
app/playwright.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
webServer: { command: 'pnpm build && pnpm preview', port: 4173 },
|
||||
testDir: 'e2e',
|
||||
use: {
|
||||
browserName: 'firefox',
|
||||
launchOptions: {
|
||||
firefoxUserPrefs: {
|
||||
// Force WebGL even without a GPU
|
||||
'webgl.force-enabled': true,
|
||||
'webgl.disabled': false,
|
||||
// Use software rendering (Mesa) instead of hardware
|
||||
'layers.acceleration.disabled': true,
|
||||
'gfx.webrender.software': true,
|
||||
'webgl.enable-webgl2': true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2,5 +2,9 @@
|
||||
@source "../../packages/ui/**/*.svelte";
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
}
|
||||
|
||||
input {
|
||||
background: var(--layer-0);
|
||||
background: var(--color-layer-0);
|
||||
font-family: var(--font-family);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
@@ -168,10 +168,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 +184,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--layer-2);
|
||||
background: var(--color-layer-2);
|
||||
opacity: 0;
|
||||
}
|
||||
input:disabled {
|
||||
@@ -264,8 +264,8 @@
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 25px);
|
||||
border: dashed 4px var(--layer-2);
|
||||
background: var(--layer-1);
|
||||
border: dashed 4px var(--color-layer-2);
|
||||
background: var(--color-layer-1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@ const variables = [
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
svg path {
|
||||
stroke-width: 0.2px;
|
||||
transition: d 0.3s ease, fill 0.3s ease;
|
||||
fill: var(--layer-2);
|
||||
fill: var(--color-layer-2);
|
||||
stroke: var(--stroke);
|
||||
stroke-width: var(--stroke-width);
|
||||
d: var(--path);
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
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);
|
||||
|
||||
110
app/src/lib/graph-templates.test.ts
Normal file
110
app/src/lib/graph-templates.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { grid } from '$lib/graph-templates/grid';
|
||||
import { tree } from '$lib/graph-templates/tree';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('graph-templates', () => {
|
||||
describe('grid', () => {
|
||||
it('should create a grid graph with nodes and edges', () => {
|
||||
const result = grid(2, 3);
|
||||
expect(result.nodes.length).toBeGreaterThan(0);
|
||||
expect(result.edges.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have output node at the end', () => {
|
||||
const result = grid(1, 1);
|
||||
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||
expect(outputNode).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create nodes based on grid dimensions', () => {
|
||||
const result = grid(2, 2);
|
||||
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
|
||||
expect(mathNodes.length).toBeGreaterThan(0);
|
||||
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||
expect(outputNode).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have output node at the end', () => {
|
||||
const result = grid(1, 1);
|
||||
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||
expect(outputNode).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create nodes based on grid dimensions', () => {
|
||||
const result = grid(2, 2);
|
||||
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
|
||||
expect(mathNodes.length).toBeGreaterThan(0);
|
||||
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||
expect(outputNode).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have valid node positions', () => {
|
||||
const result = grid(3, 2);
|
||||
|
||||
result.nodes.forEach(node => {
|
||||
expect(node.position).toHaveLength(2);
|
||||
expect(typeof node.position[0]).toBe('number');
|
||||
expect(typeof node.position[1]).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate valid graph structure', () => {
|
||||
const result = grid(2, 2);
|
||||
|
||||
result.nodes.forEach(node => {
|
||||
expect(typeof node.id).toBe('number');
|
||||
expect(node.type).toBeTruthy();
|
||||
});
|
||||
|
||||
result.edges.forEach(edge => {
|
||||
expect(edge).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tree', () => {
|
||||
it('should create a tree graph with specified depth', () => {
|
||||
const result = tree(0);
|
||||
|
||||
expect(result.nodes.length).toBeGreaterThan(0);
|
||||
expect(result.edges.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have root output node', () => {
|
||||
const result = tree(2);
|
||||
|
||||
const outputNode = result.nodes.find(n => n.type === 'max/plantarium/output');
|
||||
expect(outputNode).toBeDefined();
|
||||
expect(outputNode?.id).toBe(0);
|
||||
});
|
||||
|
||||
it('should increase node count with depth', () => {
|
||||
const tree0 = tree(0);
|
||||
const tree1 = tree(1);
|
||||
const tree2 = tree(2);
|
||||
|
||||
expect(tree0.nodes.length).toBeLessThan(tree1.nodes.length);
|
||||
expect(tree1.nodes.length).toBeLessThan(tree2.nodes.length);
|
||||
});
|
||||
|
||||
it('should create binary tree structure', () => {
|
||||
const result = tree(2);
|
||||
|
||||
const mathNodes = result.nodes.filter(n => n.type === 'max/plantarium/math');
|
||||
expect(mathNodes.length).toBeGreaterThan(0);
|
||||
|
||||
const edgeCount = result.edges.length;
|
||||
expect(edgeCount).toBe(result.nodes.length - 1);
|
||||
});
|
||||
|
||||
it('should have valid node positions', () => {
|
||||
const result = tree(3);
|
||||
|
||||
result.nodes.forEach(node => {
|
||||
expect(node.position).toHaveLength(2);
|
||||
expect(typeof node.position[0]).toBe('number');
|
||||
expect(typeof node.position[1]).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
63
app/src/lib/graph-templates/simple.json
Normal file
63
app/src/lib/graph-templates/simple.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": 0,
|
||||
"settings": {
|
||||
"resolution.circle": 54,
|
||||
"resolution.curve": 20,
|
||||
"randomSeed": true
|
||||
},
|
||||
"meta": {
|
||||
"title": "New Project",
|
||||
"lastModified": "2026-02-03T16:56:40.375Z"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": 9,
|
||||
"position": [
|
||||
215,
|
||||
85
|
||||
],
|
||||
"type": "max/plantarium/output",
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"position": [
|
||||
165,
|
||||
72.5
|
||||
],
|
||||
"type": "max/plantarium/stem",
|
||||
"props": {
|
||||
"amount": 50,
|
||||
"length": 4,
|
||||
"thickness": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"position": [
|
||||
190,
|
||||
77.5
|
||||
],
|
||||
"type": "max/plantarium/noise",
|
||||
"props": {
|
||||
"plant": 0,
|
||||
"scale": 0.5,
|
||||
"strength": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
[
|
||||
10,
|
||||
0,
|
||||
11,
|
||||
"plant"
|
||||
],
|
||||
[
|
||||
11,
|
||||
0,
|
||||
9,
|
||||
"input"
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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
145
app/src/lib/helpers.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { clone, debounce, humanizeDuration, humanizeNumber, lerp, snapToGrid } from '$lib/helpers';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('snapToGrid', () => {
|
||||
it('should snap to nearest grid point', () => {
|
||||
expect(snapToGrid(5, 10)).toBe(10);
|
||||
expect(snapToGrid(15, 10)).toBe(20);
|
||||
expect(snapToGrid(0, 10)).toBe(0);
|
||||
expect(snapToGrid(-10, 10)).toBe(-10);
|
||||
});
|
||||
|
||||
it('should snap exact midpoint values', () => {
|
||||
expect(snapToGrid(5, 10)).toBe(10);
|
||||
});
|
||||
|
||||
it('should use default grid size of 10', () => {
|
||||
expect(snapToGrid(5)).toBe(10);
|
||||
expect(snapToGrid(15)).toBe(20);
|
||||
});
|
||||
|
||||
it('should handle values exactly on grid', () => {
|
||||
expect(snapToGrid(10, 10)).toBe(10);
|
||||
expect(snapToGrid(20, 10)).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lerp', () => {
|
||||
it('should linearly interpolate between two values', () => {
|
||||
expect(lerp(0, 100, 0)).toBe(0);
|
||||
expect(lerp(0, 100, 0.5)).toBe(50);
|
||||
expect(lerp(0, 100, 1)).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle negative values', () => {
|
||||
expect(lerp(-50, 50, 0.5)).toBe(0);
|
||||
expect(lerp(-100, 0, 0.5)).toBe(-50);
|
||||
});
|
||||
|
||||
it('should handle t values outside 0-1 range', () => {
|
||||
expect(lerp(0, 100, -0.5)).toBe(-50);
|
||||
expect(lerp(0, 100, 1.5)).toBe(150);
|
||||
});
|
||||
});
|
||||
|
||||
describe('humanizeNumber', () => {
|
||||
it('should return unchanged numbers below 1000', () => {
|
||||
expect(humanizeNumber(0)).toBe('0');
|
||||
expect(humanizeNumber(999)).toBe('999');
|
||||
});
|
||||
|
||||
it('should add K suffix for thousands', () => {
|
||||
expect(humanizeNumber(1000)).toBe('1K');
|
||||
expect(humanizeNumber(1500)).toBe('1.5K');
|
||||
expect(humanizeNumber(999999)).toBe('1000K');
|
||||
});
|
||||
|
||||
it('should add M suffix for millions', () => {
|
||||
expect(humanizeNumber(1000000)).toBe('1M');
|
||||
expect(humanizeNumber(2500000)).toBe('2.5M');
|
||||
});
|
||||
|
||||
it('should add B suffix for billions', () => {
|
||||
expect(humanizeNumber(1000000000)).toBe('1B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('humanizeDuration', () => {
|
||||
it('should return ms for very short durations', () => {
|
||||
expect(humanizeDuration(100)).toBe('100ms');
|
||||
expect(humanizeDuration(999)).toBe('999ms');
|
||||
});
|
||||
|
||||
it('should format seconds', () => {
|
||||
expect(humanizeDuration(1000)).toBe('1s');
|
||||
expect(humanizeDuration(1500)).toBe('1s500ms');
|
||||
expect(humanizeDuration(59000)).toBe('59s');
|
||||
});
|
||||
|
||||
it('should format minutes', () => {
|
||||
expect(humanizeDuration(60000)).toBe('1m');
|
||||
expect(humanizeDuration(90000)).toBe('1m 30s');
|
||||
});
|
||||
|
||||
it('should format hours', () => {
|
||||
expect(humanizeDuration(3600000)).toBe('1h');
|
||||
expect(humanizeDuration(3661000)).toBe('1h 1m 1s');
|
||||
});
|
||||
|
||||
it('should format days', () => {
|
||||
expect(humanizeDuration(86400000)).toBe('1d');
|
||||
expect(humanizeDuration(90061000)).toBe('1d 1h 1m 1s');
|
||||
});
|
||||
|
||||
it('should handle zero', () => {
|
||||
expect(humanizeDuration(0)).toBe('0ms');
|
||||
});
|
||||
});
|
||||
|
||||
describe('debounce', () => {
|
||||
it('should return a function', () => {
|
||||
const fn = debounce(() => {}, 100);
|
||||
expect(typeof fn).toBe('function');
|
||||
});
|
||||
|
||||
it('should only call once when invoked multiple times within delay', () => {
|
||||
let callCount = 0;
|
||||
const fn = debounce(() => {
|
||||
callCount++;
|
||||
}, 100);
|
||||
fn();
|
||||
const firstCall = callCount;
|
||||
fn();
|
||||
fn();
|
||||
expect(callCount).toBe(firstCall);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clone', () => {
|
||||
it('should deep clone objects', () => {
|
||||
const original = { a: 1, b: { c: 2 } };
|
||||
const cloned = clone(original);
|
||||
|
||||
expect(cloned).toEqual(original);
|
||||
expect(cloned).not.toBe(original);
|
||||
expect(cloned.b).not.toBe(original.b);
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const original = [1, 2, [3, 4]];
|
||||
const cloned = clone(original);
|
||||
|
||||
expect(cloned).toEqual(original);
|
||||
expect(cloned).not.toBe(original);
|
||||
expect(cloned[2]).not.toBe(original[2]);
|
||||
});
|
||||
|
||||
it('should handle primitives', () => {
|
||||
expect(clone(42)).toBe(42);
|
||||
expect(clone('hello')).toBe('hello');
|
||||
expect(clone(true)).toBe(true);
|
||||
expect(clone(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
72
app/src/lib/helpers/deepMerge.test.ts
Normal file
72
app/src/lib/helpers/deepMerge.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { isObject, mergeDeep } from '$lib/helpers/deepMerge';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('deepMerge', () => {
|
||||
describe('isObject', () => {
|
||||
it('should return true for plain objects', () => {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-objects', () => {
|
||||
expect(isObject([])).toBe(false);
|
||||
expect(isObject('string')).toBe(false);
|
||||
expect(isObject(42)).toBe(false);
|
||||
expect(isObject(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeDeep', () => {
|
||||
it('should merge two flat objects', () => {
|
||||
const target = { a: 1, b: 2 };
|
||||
const source = { b: 3, c: 4 };
|
||||
const result = mergeDeep(target, source);
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 3, c: 4 });
|
||||
});
|
||||
|
||||
it('should deeply merge nested objects', () => {
|
||||
const target = { a: { x: 1 }, b: { y: 2 } };
|
||||
const source = { a: { y: 2 }, c: { z: 3 } };
|
||||
const result = mergeDeep(target, source);
|
||||
|
||||
expect(result).toEqual({
|
||||
a: { x: 1, y: 2 },
|
||||
b: { y: 2 },
|
||||
c: { z: 3 }
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple sources', () => {
|
||||
const target = { a: 1 };
|
||||
const source1 = { b: 2 };
|
||||
const source2 = { c: 3 };
|
||||
const result = mergeDeep(target, source1, source2);
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('should return target if no sources provided', () => {
|
||||
const target = { a: 1 };
|
||||
const result = mergeDeep(target);
|
||||
|
||||
expect(result).toBe(target);
|
||||
});
|
||||
|
||||
it('should overwrite non-object values', () => {
|
||||
const target = { a: { b: 1 } };
|
||||
const source = { a: 'string' };
|
||||
const result = mergeDeep(target, source);
|
||||
|
||||
expect(result.a).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle arrays by replacing', () => {
|
||||
const target = { a: [1, 2] };
|
||||
const source = { a: [3, 4] };
|
||||
const result = mergeDeep(target, source);
|
||||
|
||||
expect(result.a).toEqual([3, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
tabindex="0"
|
||||
ondragstart={handleDragStart}
|
||||
>
|
||||
{#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;
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
display: flex;
|
||||
}
|
||||
p {
|
||||
@@ -88,13 +88,13 @@
|
||||
svg {
|
||||
height: 124px;
|
||||
margin: 24px 0px;
|
||||
border-top: solid thin var(--outline);
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-top: solid thin var(--color-outline);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
width: 100%;
|
||||
}
|
||||
polyline {
|
||||
fill: none;
|
||||
stroke: var(--layer-3);
|
||||
stroke: var(--color-layer-3);
|
||||
opacity: 0.5;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -1,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>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
polyline {
|
||||
fill: none;
|
||||
stroke: var(--layer-3);
|
||||
stroke: var(--color-layer-3);
|
||||
opacity: 1;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!);
|
||||
}}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
&& typeof internalValue === 'number'
|
||||
) {
|
||||
value[key] = node?.options?.[internalValue];
|
||||
} else if (internalValue) {
|
||||
} else if (internalValue !== undefined) {
|
||||
value[key] = internalValue;
|
||||
}
|
||||
});
|
||||
@@ -124,7 +124,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 +137,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 +148,6 @@
|
||||
{/each}
|
||||
<hr />
|
||||
{:else if key && type?.[key]}
|
||||
<!-- Group -->
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
@@ -211,6 +208,11 @@
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
padding-bottom: 1px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.first-level.input-boolean {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -218,11 +220,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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<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 +44,7 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--layer-1);
|
||||
background: var(--color-layer-1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
@@ -55,7 +54,7 @@
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: solid thin var(--outline);
|
||||
border-right: solid thin var(--color-outline);
|
||||
}
|
||||
|
||||
.tabs > button {
|
||||
@@ -66,9 +65,9 @@
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-left: solid thin var(--outline);
|
||||
background: var(--layer-1);
|
||||
border-bottom: solid thin var(--color-outline);
|
||||
border-left: solid thin var(--color-outline);
|
||||
background: var(--color-layer-1);
|
||||
}
|
||||
|
||||
.tabs > button > span {
|
||||
@@ -76,7 +75,7 @@
|
||||
}
|
||||
|
||||
.tabs > button.active {
|
||||
background: var(--layer-2);
|
||||
background: var(--color-layer-2);
|
||||
}
|
||||
|
||||
.tabs > button.active span {
|
||||
|
||||
@@ -95,5 +95,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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -247,12 +247,6 @@
|
||||
type={graphSettingTypes}
|
||||
bind:value={graphSettings}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel
|
||||
id="active-node"
|
||||
title="Node Settings"
|
||||
icon="i-[tabler--adjustments] bg-blue-400"
|
||||
>
|
||||
<ActiveNodeSettings {manager} bind:node={activeNode} />
|
||||
</Panel>
|
||||
</Sidebar>
|
||||
@@ -262,7 +256,7 @@
|
||||
|
||||
<style>
|
||||
header {
|
||||
background-color: var(--layer-1);
|
||||
background-color: var(--color-layer-1);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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}']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,16 +5,19 @@
|
||||
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,
|
||||
face_amount,\
|
||||
instance_amount,
|
||||
stem_depth,
|
||||
...faces...,
|
||||
@@ -22,4 +25,3 @@ Plantarium encodes different types of data differently. Right now we have three
|
||||
...normals...,
|
||||
...4x4 matrices for instances...
|
||||
]
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -8,7 +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" }
|
||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||
noise = "0.9.0"
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -7,7 +7,6 @@ edition = "2018"
|
||||
[lib]
|
||||
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" }
|
||||
|
||||
@@ -8,7 +8,7 @@ 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" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
glam = "0.30.10"
|
||||
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
|
||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -8,7 +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" }
|
||||
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
{
|
||||
"version": "0.0.2",
|
||||
"scripts": {
|
||||
"postinstall": "pnpm run -r --filter 'ui' build",
|
||||
"build": "pnpm build:nodes && pnpm build:app",
|
||||
"lint": "pnpm run -r --parallel lint",
|
||||
"format": "pnpm run -r format",
|
||||
"format:check": "pnpm run -r format:check",
|
||||
"format": "pnpm dprint fmt",
|
||||
"format:check": "pnpm dprint check",
|
||||
"test": "pnpm run -r test",
|
||||
"check": "pnpm run -r check",
|
||||
"build:story": "pnpm -r --filter 'ui' story:build",
|
||||
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
|
||||
"build:nodes": "cargo build --workspace --target wasm32-unknown-unknown --release && rm -rf ./app/static/nodes/max/plantarium/ && mkdir -p ./app/static/nodes/max/plantarium/ && cp -R ./target/wasm32-unknown-unknown/release/*.wasm ./app/static/nodes/max/plantarium/",
|
||||
"build:deploy": "pnpm build",
|
||||
"build:deploy": "pnpm build && cp -R packages/ui/build app/build/ui",
|
||||
"dev:nodes": "chokidar './nodes/**' --initial -i '/pkg/' -c 'pnpm build:nodes'",
|
||||
"dev:app_ui": "pnpm -r --parallel --filter 'app' --filter './packages/ui' dev",
|
||||
"dev_ui": "pnpm -r --filter 'ui' dev:ui",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodarium/types",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "@nodarium/ui",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"scripts": {
|
||||
"dev": "chokidar './src/**' --initial -c 'pnpm build'",
|
||||
"dev:ui": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build && npm run package",
|
||||
"preview": "vite preview",
|
||||
"package": "svelte-kit sync && svelte-package && publint",
|
||||
@@ -35,15 +34,17 @@
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@nodarium/types": "workspace:",
|
||||
"@playwright/test": "^1.58.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.50.0",
|
||||
"@sveltejs/package": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@testing-library/svelte": "^5.3.1",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/three": "^0.182.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"dprint": "^0.51.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
@@ -56,12 +57,15 @@
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.17"
|
||||
"vitest": "^4.0.18",
|
||||
"vitest-browser-svelte": "^2.0.2"
|
||||
},
|
||||
"svelte": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@iconify-json/tabler": "^1.2.26",
|
||||
"@iconify/tailwind4": "^1.2.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@threlte/core": "^8.3.1",
|
||||
"@threlte/extras": "^9.7.1",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
interface Props {
|
||||
title?: string;
|
||||
transparent?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
children?: Snippet;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
let { title = 'Details', transparent = false, children, open = $bindable(false) }: Props =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<details class:transparent bind:open>
|
||||
<details class:transparent bind:open class="text-text outline-1 outline-outline bg-layer-1">
|
||||
<summary>{title}</summary>
|
||||
<div class="content">
|
||||
{@render children?.()}
|
||||
@@ -20,10 +20,7 @@
|
||||
<style>
|
||||
details {
|
||||
padding: 1em;
|
||||
color: var(--text-color);
|
||||
padding-left: 20px;
|
||||
background-color: #202020;
|
||||
outline: solid 0.1px white;
|
||||
border-radius: 2px;
|
||||
font-weight: 300;
|
||||
font-size: 0.9em;
|
||||
|
||||
13
packages/ui/src/lib/Details.svelte.ts
Normal file
13
packages/ui/src/lib/Details.svelte.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import Details from './Details.svelte';
|
||||
|
||||
describe('Details', () => {
|
||||
it('should render summary element', async () => {
|
||||
render(Details, { title: 'Click me' });
|
||||
|
||||
const summary = page.getByText('Click me');
|
||||
await expect.element(summary).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { NodeInput } from '@nodarium/types';
|
||||
|
||||
import { Checkbox, Float, Select, Vec3 } from './index.js';
|
||||
import { InputCheckbox, InputNumber, InputSelect, InputVec3 } from './index';
|
||||
|
||||
interface Props {
|
||||
input: NodeInput;
|
||||
@@ -13,18 +13,18 @@
|
||||
</script>
|
||||
|
||||
{#if input.type === 'float'}
|
||||
<Float
|
||||
<InputNumber
|
||||
bind:value={value as number}
|
||||
min={input?.min}
|
||||
max={input?.max}
|
||||
step={input?.step}
|
||||
/>
|
||||
{:else if input.type === 'integer'}
|
||||
<Float bind:value={value as number} min={input?.min} max={input?.max} />
|
||||
<InputNumber bind:value={value as number} min={input?.min} max={input?.max} step={1} />
|
||||
{:else if input.type === 'boolean'}
|
||||
<Checkbox bind:value={value as boolean} {id} />
|
||||
<InputCheckbox bind:value={value as boolean} {id} />
|
||||
{:else if input.type === 'select'}
|
||||
<Select bind:value={value as number} options={input.options} {id} />
|
||||
<InputSelect bind:value={value as number} options={input.options} {id} />
|
||||
{:else if input.type === 'vec3'}
|
||||
<Vec3 bind:value={value as [number, number, number]} {id} />
|
||||
<InputVec3 bind:value={value as [number, number, number]} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
let { ctrl = false, shift = false, alt = false, key }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="command">
|
||||
<div class="command bg-layer-2">
|
||||
{#if ctrl}
|
||||
<span>Ctrl</span>
|
||||
{/if}
|
||||
@@ -24,8 +24,8 @@
|
||||
|
||||
<style>
|
||||
.command {
|
||||
background: var(--layer-2);
|
||||
padding: 0.4em;
|
||||
padding-inline: 0.8em;
|
||||
font-size: 0.8em;
|
||||
border-radius: 0.3em;
|
||||
white-space: nowrap;
|
||||
|
||||
34
packages/ui/src/lib/ShortCut.svelte.ts
Normal file
34
packages/ui/src/lib/ShortCut.svelte.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import ShortCut from './ShortCut.svelte';
|
||||
|
||||
describe('ShortCut', () => {
|
||||
it('should render with key label', async () => {
|
||||
render(ShortCut, { key: 'S' });
|
||||
|
||||
const shortcut = page.getByText('S');
|
||||
await expect.element(shortcut).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ctrl modifier', async () => {
|
||||
render(ShortCut, { ctrl: true, key: 'S' });
|
||||
|
||||
const shortcut = page.getByText(/Ctrl/);
|
||||
await expect.element(shortcut).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render alt modifier', async () => {
|
||||
render(ShortCut, { alt: true, key: 'F4' });
|
||||
|
||||
const shortcut = page.getByText(/Alt/);
|
||||
await expect.element(shortcut).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render multiple modifiers', async () => {
|
||||
render(ShortCut, { ctrl: true, alt: true, key: 'Delete' });
|
||||
|
||||
const shortcut = page.getByText(/Ctrl/);
|
||||
await expect.element(shortcut).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,18 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@iconify/tailwind4" {
|
||||
prefix: "i";
|
||||
}
|
||||
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-0");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-1");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-2");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-3");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}active");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}selected");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}outline{!,}");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}connection");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}edge");
|
||||
@source inline("{hover:,}{bg-,outline-,text-,}text");
|
||||
|
||||
/* fira-code-300 - latin */
|
||||
@font-face {
|
||||
@@ -58,9 +72,9 @@
|
||||
|
||||
--color-outline: var(--neutral-400);
|
||||
--color-connection: #333333;
|
||||
--color-edge: var(--connection, var(--outline));
|
||||
--color-edge: var(--connection, var(--color-outline));
|
||||
|
||||
--color-text-color: var(--neutral-200);
|
||||
--color-text: var(--neutral-200);
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -72,100 +86,89 @@ html {
|
||||
--neutral-800: #111111;
|
||||
--neutral-900: #060606;
|
||||
|
||||
--layer-0: var(--neutral-900);
|
||||
--layer-1: var(--neutral-500);
|
||||
--layer-2: var(--neutral-400);
|
||||
--layer-3: var(--neutral-200);
|
||||
--color-layer-0: var(--neutral-900);
|
||||
--color-layer-1: var(--neutral-500);
|
||||
--color-layer-2: var(--neutral-400);
|
||||
--color-layer-3: var(--neutral-200);
|
||||
|
||||
--active: #ffffff;
|
||||
--selected: #c65a19;
|
||||
--color-active: #ffffff;
|
||||
--color-selected: #c65a19;
|
||||
|
||||
--outline: var(--neutral-400);
|
||||
--connection: #333333;
|
||||
--edge: var(--connection, var(--outline));
|
||||
--color-outline: var(--neutral-400);
|
||||
--color-connection: #333333;
|
||||
--color-edge: var(--connection, var(--color-outline));
|
||||
|
||||
--text-color: var(--neutral-200);
|
||||
--color-text-color: var(--neutral-200);
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--layer-0);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-layer-0);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body * {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
html.theme-light {
|
||||
--text-color: var(--neutral-800);
|
||||
--outline: var(--neutral-300);
|
||||
--layer-0: var(--neutral-100);
|
||||
--layer-1: var(--neutral-100);
|
||||
--layer-2: var(--neutral-200);
|
||||
--layer-3: var(--neutral-500);
|
||||
--active: #000000;
|
||||
--selected: #c65a19;
|
||||
--connection: #888;
|
||||
--color-text: var(--neutral-800);
|
||||
--color-outline: var(--neutral-300);
|
||||
--color-layer-0: var(--neutral-100);
|
||||
--color-layer-1: var(--neutral-100);
|
||||
--color-layer-2: var(--neutral-200);
|
||||
--color-layer-3: var(--neutral-500);
|
||||
--color-active: #000000;
|
||||
--color-selected: #c65a19;
|
||||
--color-connection: #888;
|
||||
}
|
||||
|
||||
html.theme-solarized {
|
||||
--text-color: #425055;
|
||||
--outline: #93a1a1;
|
||||
--layer-0: #fdf6e3;
|
||||
--layer-1: #eee8d5;
|
||||
--layer-2: #c4c0b4;
|
||||
--layer-3: #586e75;
|
||||
--active: #000000;
|
||||
--selected: #268bd2;
|
||||
--connection: #839496;
|
||||
--color-text: #425055;
|
||||
--color-outline: #93a1a1;
|
||||
--color-layer-0: #fdf6e3;
|
||||
--color-layer-1: #eee8d5;
|
||||
--color-layer-2: #c4c0b4;
|
||||
--color-layer-3: #586e75;
|
||||
--color-active: #000000;
|
||||
--color-selected: #268bd2;
|
||||
--color-connection: #839496;
|
||||
}
|
||||
|
||||
html.theme-catppuccin {
|
||||
--text-color: #cdd6f4;
|
||||
--outline: #3e3e4f;
|
||||
--layer-3: #a8aac8;
|
||||
--layer-2: #313244;
|
||||
--layer-1: #1e1e2e;
|
||||
--layer-0: #11111b;
|
||||
--connection: #444459;
|
||||
--color-text: #cdd6f4;
|
||||
--color-outline: #3e3e4f;
|
||||
--color-layer-3: #a8aac8;
|
||||
--color-layer-2: #313244;
|
||||
--color-layer-1: #1e1e2e;
|
||||
--color-layer-0: #11111b;
|
||||
--color-connection: #444459;
|
||||
}
|
||||
|
||||
html.theme-high-contrast {
|
||||
--text-color: #ffffff;
|
||||
--outline: white;
|
||||
--layer-0: #000000;
|
||||
--layer-1: black;
|
||||
--layer-2: #222222;
|
||||
--layer-3: #ffffff;
|
||||
--connection: #fff;
|
||||
--color-text: #ffffff;
|
||||
--color-outline: white;
|
||||
--color-layer-0: #000000;
|
||||
--color-layer-1: black;
|
||||
--color-layer-2: #222222;
|
||||
--color-layer-3: #ffffff;
|
||||
--color-connection: #fff;
|
||||
}
|
||||
|
||||
html.theme-nord {
|
||||
--text-color: #d8dee9;
|
||||
--outline: #4c566a;
|
||||
--layer-0: #2e3440;
|
||||
--layer-1: #3b4252;
|
||||
--layer-2: #434c5e;
|
||||
--layer-3: #5e81ac;
|
||||
--active: #8999bd;
|
||||
--selected: #b76c3f;
|
||||
--connection: #4c566a;
|
||||
--color-text: #d8dee9;
|
||||
--color-outline: #4c566a;
|
||||
--color-layer-0: #2e3440;
|
||||
--color-layer-1: #3b4252;
|
||||
--color-layer-2: #434c5e;
|
||||
--color-layer-3: #5e81ac;
|
||||
--color-active: #8999bd;
|
||||
--color-selected: #b76c3f;
|
||||
--color-connection: #4c566a;
|
||||
}
|
||||
|
||||
html.theme-dracula {
|
||||
--text-color: #f8f8f2;
|
||||
--outline: #6272a4;
|
||||
--layer-0: #282a36;
|
||||
--layer-1: #44475a;
|
||||
--layer-2: #32374d;
|
||||
--layer-3: #bd93f9;
|
||||
--connection: #6272a4;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--layer-2);
|
||||
border: 1px solid var(--outline);
|
||||
padding: 8px 9px;
|
||||
border-radius: 4px;
|
||||
--color-text: #f8f8f2;
|
||||
--color-outline: #6272a4;
|
||||
--color-layer-0: #282a36;
|
||||
--color-layer-1: #44475a;
|
||||
--color-layer-2: #32374d;
|
||||
--color-layer-3: #bd93f9;
|
||||
--color-connection: #6272a4;
|
||||
}
|
||||
|
||||
55
packages/ui/src/lib/helpers/getBoundingValue.test.ts
Normal file
55
packages/ui/src/lib/helpers/getBoundingValue.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getBoundingValue } from './getBoundingValue';
|
||||
|
||||
describe('getBoundingValue', () => {
|
||||
it('should return 1 for values between 0 and 1', () => {
|
||||
expect(getBoundingValue(0)).toBe(1);
|
||||
expect(getBoundingValue(0.5)).toBe(1);
|
||||
expect(getBoundingValue(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 2 for values between 1 and 2', () => {
|
||||
expect(getBoundingValue(1.1)).toBe(2);
|
||||
expect(getBoundingValue(1.5)).toBe(2);
|
||||
expect(getBoundingValue(2)).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 4 for values between 2 and 4', () => {
|
||||
expect(getBoundingValue(2.1)).toBe(4);
|
||||
expect(getBoundingValue(3)).toBe(4);
|
||||
expect(getBoundingValue(4)).toBe(4);
|
||||
});
|
||||
|
||||
it('should return positive level for positive input', () => {
|
||||
expect(getBoundingValue(5)).toBe(10);
|
||||
expect(getBoundingValue(15)).toBe(20);
|
||||
expect(getBoundingValue(50)).toBe(50);
|
||||
expect(getBoundingValue(150)).toBe(200);
|
||||
});
|
||||
|
||||
it('should return negative level for negative input', () => {
|
||||
expect(getBoundingValue(-5)).toBe(-10);
|
||||
expect(getBoundingValue(-15)).toBe(-20);
|
||||
expect(getBoundingValue(-50)).toBe(-50);
|
||||
expect(getBoundingValue(-150)).toBe(-200);
|
||||
});
|
||||
|
||||
it('should return correct level for boundary values', () => {
|
||||
expect(getBoundingValue(10)).toBe(10);
|
||||
expect(getBoundingValue(20)).toBe(20);
|
||||
expect(getBoundingValue(50)).toBe(50);
|
||||
expect(getBoundingValue(100)).toBe(100);
|
||||
expect(getBoundingValue(200)).toBe(200);
|
||||
});
|
||||
|
||||
it('should handle large values', () => {
|
||||
expect(getBoundingValue(1000)).toBe(1000);
|
||||
expect(getBoundingValue(1500)).toBe(1000);
|
||||
expect(getBoundingValue(-1000)).toBe(-1000);
|
||||
});
|
||||
|
||||
it('should handle very small values', () => {
|
||||
expect(getBoundingValue(0.001)).toBe(1);
|
||||
expect(getBoundingValue(-0.001)).toBe(-1);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
export { default as Input } from './Input.svelte';
|
||||
export { default as Checkbox } from './inputs/Checkbox.svelte';
|
||||
export { default as Float } from './inputs/Float.svelte';
|
||||
export { default as Select } from './inputs/Select.svelte';
|
||||
export { default as Vec3 } from './inputs/Vec3.svelte';
|
||||
export { default as InputCheckbox } from './inputs/InputCheckbox.svelte';
|
||||
export { default as InputNumber } from './inputs/InputNumber.svelte';
|
||||
export { default as InputSelect } from './inputs/InputSelect.svelte';
|
||||
export { default as InputVec3 } from './inputs/InputVec3.svelte';
|
||||
|
||||
export { default as Details } from './Details.svelte';
|
||||
export { default as ShortCut } from './ShortCut.svelte';
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
value?: number;
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(0.5),
|
||||
step,
|
||||
min = $bindable(0),
|
||||
max = $bindable(1),
|
||||
id
|
||||
}: Props = $props();
|
||||
|
||||
if (min > max) {
|
||||
[min, max] = [max, min];
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
|
||||
const precision = $derived(
|
||||
((step || value).toString().split('.')[1] || '').length
|
||||
);
|
||||
|
||||
function strip(input: number) {
|
||||
return +parseFloat(input + '').toFixed(precision);
|
||||
}
|
||||
|
||||
let inputEl: HTMLInputElement | undefined = $state();
|
||||
|
||||
let oldValue: number;
|
||||
function handleChange() {
|
||||
if (value === oldValue) return;
|
||||
oldValue = value;
|
||||
}
|
||||
|
||||
let isMouseDown = $state(false);
|
||||
let downV = 0;
|
||||
let vx = 0;
|
||||
let rect: DOMRect;
|
||||
|
||||
function handleMouseDown(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!inputEl) return;
|
||||
|
||||
inputEl.focus();
|
||||
|
||||
isMouseDown = true;
|
||||
|
||||
downV = value;
|
||||
rect = inputEl.getBoundingClientRect();
|
||||
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
(ev.target as HTMLElement)?.blur();
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isMouseDown = false;
|
||||
|
||||
if (downV === value) {
|
||||
inputEl?.focus();
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'unset';
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
|
||||
function handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === 'Escape' || ev.key === 'Enter') {
|
||||
handleMouseUp();
|
||||
inputEl?.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(ev: MouseEvent) {
|
||||
vx = (ev.clientX - rect.left) / rect.width;
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
let v = min + (max - min) * vx;
|
||||
value = v;
|
||||
} else {
|
||||
value = Math.max(Math.min(min + (max - min) * vx, max), min);
|
||||
}
|
||||
|
||||
value = strip(value);
|
||||
|
||||
// With ctrl + outside of input ev.target becomes HTMLDocument
|
||||
if (ev.target instanceof HTMLElement) {
|
||||
ev.target?.blur();
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (value.toString().length > 5) {
|
||||
value = strip(value);
|
||||
}
|
||||
if (value !== undefined) {
|
||||
handleChange();
|
||||
}
|
||||
});
|
||||
|
||||
let width = $derived(
|
||||
Number.isFinite(value)
|
||||
? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px'
|
||||
: '20px'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="component-wrapper" class:is-down={isMouseDown}>
|
||||
<span class="overlay" style={`width: ${((value - min) / (max - min)) * 100}%`}></span>
|
||||
<input
|
||||
bind:value
|
||||
bind:this={inputEl}
|
||||
{id}
|
||||
{step}
|
||||
{max}
|
||||
{min}
|
||||
onkeydown={handleKeyDown}
|
||||
onmousedown={handleMouseDown}
|
||||
onmouseup={handleMouseUp}
|
||||
type="number"
|
||||
style={`width:${width};`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.component-wrapper {
|
||||
position: relative;
|
||||
background-color: var(--layer-2, #4b4b4b);
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.3s ease;
|
||||
border: solid 1px var(--outline);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius, 2px);
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
font-family: var(--font-family);
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: pointer;
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
padding: var(--padding, 6px);
|
||||
font-size: 1em;
|
||||
padding-inline: 10px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-style: none;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.is-down > input {
|
||||
cursor: ew-resize !important;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--text-color);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.is-down > .overlay {
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@
|
||||
</script>
|
||||
|
||||
<label
|
||||
class="relative inline-flex h-[22px] w-[22px] cursor-pointer items-center justify-center bg-[var(--layer-2)] rounded-[5px]"
|
||||
class="relative inline-flex h-5.5 w-5.5 cursor-pointer items-center justify-center bg-layer-2 rounded-[5px]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -33,7 +33,7 @@
|
||||
viewBox="0 0 19 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-[10px] w-[12px] text-[var(--text-color)]"
|
||||
class="h-2.5 w-3 text-text"
|
||||
>
|
||||
<path
|
||||
d="M2 7L7 12L17 2"
|
||||
27
packages/ui/src/lib/inputs/InputCheckbox.svelte.ts
Normal file
27
packages/ui/src/lib/inputs/InputCheckbox.svelte.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import InputCheckbox from './InputCheckbox.svelte';
|
||||
|
||||
describe('InputCheckbox', () => {
|
||||
it('should render checkbox label', async () => {
|
||||
render(InputCheckbox, { value: false });
|
||||
|
||||
const checkbox = page.getByRole('checkbox');
|
||||
await expect.element(checkbox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be unchecked when value is false', async () => {
|
||||
render(InputCheckbox, { value: false });
|
||||
|
||||
const input = page.getByRole('checkbox');
|
||||
await expect.element(input).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should be checked when value is true', async () => {
|
||||
render(InputCheckbox, { value: true });
|
||||
|
||||
const input = page.getByRole('checkbox');
|
||||
await expect.element(input).toBeChecked();
|
||||
});
|
||||
});
|
||||
174
packages/ui/src/lib/inputs/InputNumber.svelte
Normal file
174
packages/ui/src/lib/inputs/InputNumber.svelte
Normal file
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
value?: number;
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(0.5),
|
||||
step,
|
||||
min = $bindable(0),
|
||||
max = $bindable(1),
|
||||
id
|
||||
}: Props = $props();
|
||||
|
||||
// normalize bounds
|
||||
if (min > max) [min, max] = [max, min];
|
||||
if (value > max) max = value;
|
||||
|
||||
let inputEl: HTMLInputElement | undefined = $state();
|
||||
|
||||
function clamp(v: number) {
|
||||
return Math.min(max, Math.max(min, v));
|
||||
}
|
||||
|
||||
function snap(v: number) {
|
||||
if (step) v = Math.round(v / step) * step;
|
||||
return +v.toFixed(3);
|
||||
}
|
||||
|
||||
let dragging = $state(false);
|
||||
let startValue = 0;
|
||||
let rect: DOMRect;
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if (!inputEl) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
dragging = true;
|
||||
startValue = value;
|
||||
rect = inputEl.getBoundingClientRect();
|
||||
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
const ratio = (e.clientX - rect.left) / rect.width;
|
||||
let delta = (min + (max - min) * ratio) - startValue;
|
||||
|
||||
if (e.shiftKey) delta /= 5;
|
||||
|
||||
value = snap(
|
||||
e.ctrlKey
|
||||
? startValue + delta
|
||||
: clamp(startValue + delta)
|
||||
);
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
dragging = false;
|
||||
document.body.style.cursor = '';
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
if (startValue === value) {
|
||||
inputEl?.focus();
|
||||
}
|
||||
|
||||
min = Math.min(min, value);
|
||||
max = Math.max(max, value);
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' || e.key === 'Enter') {
|
||||
onMouseUp();
|
||||
inputEl?.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function windowKeyDown(e: KeyboardEvent) {
|
||||
if (!dragging) return;
|
||||
if (e.shiftKey) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
const width = $derived(
|
||||
Number.isFinite(value)
|
||||
? `${Math.max(value.toString().length * 8, 50)}px`
|
||||
: '20px'
|
||||
);
|
||||
|
||||
function stepUp(e: MouseEvent) {
|
||||
value = snap(value + (e.shiftKey ? step! * 10 : step!));
|
||||
}
|
||||
|
||||
function stepDown(e: MouseEvent) {
|
||||
value = snap(value - (e.shiftKey ? step! * 10 : step!));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
value = snap(clamp(value));
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={windowKeyDown} />
|
||||
|
||||
<div
|
||||
class="component-wrapper relative flex items-stretch overflow-hidden rounded-sm border border-outline bg-layer-2 select-none transition-shadow"
|
||||
class:cursor-ew-resize={dragging}
|
||||
>
|
||||
{#if step}
|
||||
<button
|
||||
aria-label="step down"
|
||||
onmousedown={stepDown}
|
||||
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
|
||||
>
|
||||
<span class="i-[tabler--chevron-compact-left] block h-full w-full text-outline!"></span>
|
||||
</button>
|
||||
<div class="w-px bg-outline"></div>
|
||||
{/if}
|
||||
|
||||
<div class="relative grow">
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 bg-layer-3 opacity-30 pointer-events-none transition-[width]"
|
||||
class:transition-none={dragging}
|
||||
style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
|
||||
>
|
||||
</div>
|
||||
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
bind:value
|
||||
{id}
|
||||
{step}
|
||||
{min}
|
||||
{max}
|
||||
type="number"
|
||||
onkeydown={onKeyDown}
|
||||
onmousedown={onMouseDown}
|
||||
class="w-full min-w-full cursor-pointer bg-transparent px-2 py-1 text-center font-tabular text-text outline-none appearance-none"
|
||||
style:width={width}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if step}
|
||||
<div class="w-px bg-outline"></div>
|
||||
<button
|
||||
aria-label="step up"
|
||||
onmousedown={stepUp}
|
||||
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
|
||||
>
|
||||
<span class="i-[tabler--chevron-compact-right] block h-full w-full text-outline!"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input[type="number"] {
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
45
packages/ui/src/lib/inputs/InputNumber.svelte.ts
Normal file
45
packages/ui/src/lib/inputs/InputNumber.svelte.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import InputNumber from './InputNumber.svelte';
|
||||
|
||||
describe('InputNumber', () => {
|
||||
it('should render input element', async () => {
|
||||
render(InputNumber, { value: 0.5 });
|
||||
|
||||
const input = page.getByRole('spinbutton');
|
||||
await expect.element(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with step buttons when step is provided', async () => {
|
||||
render(InputNumber, { value: 0.5, step: 0.1 });
|
||||
|
||||
const decrementBtn = page.getByRole('button', { name: 'step down' });
|
||||
const incrementBtn = page.getByRole('button', { name: 'step up' });
|
||||
await expect.element(decrementBtn).toBeInTheDocument();
|
||||
await expect.element(incrementBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render step buttons when step is undefined', async () => {
|
||||
const screen = render(InputNumber, { value: 0.5 });
|
||||
|
||||
const buttons = screen.locator.getByRole('button');
|
||||
const count = buttons.all().length;
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should accept numeric value', async () => {
|
||||
render(InputNumber, { value: 42 });
|
||||
|
||||
const input = page.getByRole('spinbutton');
|
||||
await expect.element(input).toHaveValue(42);
|
||||
});
|
||||
|
||||
it('should accept min and max bounds', async () => {
|
||||
render(InputNumber, { value: 5, min: 0, max: 10 });
|
||||
|
||||
const input = page.getByRole('spinbutton');
|
||||
await expect.element(input).toHaveAttribute('min', '0');
|
||||
await expect.element(input).toHaveAttribute('max', '10');
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
let { options = [], value = $bindable(0), id = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<select {id} bind:value>
|
||||
<select {id} bind:value class="bg-layer-2 text-text">
|
||||
{#each options as label, i (label)}
|
||||
<option value={i}>{label}</option>
|
||||
{/each}
|
||||
@@ -16,10 +16,8 @@
|
||||
|
||||
<style>
|
||||
select {
|
||||
background: var(--layer-2);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family);
|
||||
outline: solid 1px var(--outline);
|
||||
outline: solid 1px var(--color-outline);
|
||||
padding: 0.8em 1em;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
27
packages/ui/src/lib/inputs/InputSelect.svelte.ts
Normal file
27
packages/ui/src/lib/inputs/InputSelect.svelte.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import InputSelect from './InputSelect.svelte';
|
||||
|
||||
describe('InputSelect', () => {
|
||||
it('should render select element', async () => {
|
||||
render(InputSelect, { options: ['a', 'b', 'c'], value: 0 });
|
||||
|
||||
const select = page.getByRole('combobox');
|
||||
await expect.element(select).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all options', async () => {
|
||||
render(InputSelect, { options: ['apple', 'banana', 'cherry'], value: 0 });
|
||||
|
||||
const select = page.getByRole('combobox');
|
||||
await expect.element(select).toHaveTextContent('applebananacherry');
|
||||
});
|
||||
|
||||
it('should select correct option by index', async () => {
|
||||
render(InputSelect, { options: ['first', 'second', 'third'], value: 1 });
|
||||
|
||||
const select = page.getByRole('combobox');
|
||||
await expect.element(select).toHaveTextContent('second');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Float from './Float.svelte';
|
||||
import InputNumber from './InputNumber.svelte';
|
||||
|
||||
interface Props {
|
||||
value?: number[];
|
||||
@@ -10,9 +10,9 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Float id={`${id}-x`} bind:value={value[0]} step={0.01} />
|
||||
<Float id={`${id}-y`} bind:value={value[1]} step={0.01} />
|
||||
<Float id={`${id}-z`} bind:value={value[2]} step={0.01} />
|
||||
<InputNumber id={`${id}-x`} bind:value={value[0]} step={0.01} />
|
||||
<InputNumber id={`${id}-y`} bind:value={value[1]} step={0.01} />
|
||||
<InputNumber id={`${id}-z`} bind:value={value[2]} step={0.01} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -23,9 +23,9 @@
|
||||
div > :global(.component-wrapper:nth-child(2)) {
|
||||
border-radius: 0px !important;
|
||||
outline: none;
|
||||
border: solid thin var(--outline);
|
||||
border-top: solid thin color-mix(in srgb, var(--outline) 50%, transparent);
|
||||
border-bottom: solid thin color-mix(in srgb, var(--outline) 50%, transparent);
|
||||
border: solid thin var(--color-outline);
|
||||
border-top: solid 1px var(--color-outline);
|
||||
border-bottom: solid 1px var(--color-outline);
|
||||
}
|
||||
div > :global(.component-wrapper:nth-child(3)) {
|
||||
border-top: none !important;
|
||||
26
packages/ui/src/lib/inputs/InputVec3.svelte.ts
Normal file
26
packages/ui/src/lib/inputs/InputVec3.svelte.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import InputVec3 from './InputVec3.svelte';
|
||||
|
||||
describe('InputVec3', () => {
|
||||
it('should render with correct initial values', async () => {
|
||||
const value = $state([1.5, 2.5, 3.5]);
|
||||
render(InputVec3, { value });
|
||||
|
||||
const inputs = page.getByRole('spinbutton');
|
||||
await expect.element(inputs.first()).toBeInTheDocument();
|
||||
|
||||
expect(inputs.nth(0)).toHaveValue(1.5);
|
||||
expect(inputs.nth(1)).toHaveValue(2.5);
|
||||
expect(inputs.nth(2)).toHaveValue(3.5);
|
||||
});
|
||||
|
||||
it('should have step attribute', async () => {
|
||||
const value = $state([0, 0, 0]);
|
||||
render(InputVec3, { value });
|
||||
|
||||
const input = page.getByRole('spinbutton').first();
|
||||
await expect.element(input).toHaveAttribute('step', '0.01');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import '$lib/app.css';
|
||||
import { Checkbox, Details, Float, Select, ShortCut, Vec3 } from '$lib/index.js';
|
||||
import { Details, InputCheckbox, InputNumber, InputSelect, InputVec3, ShortCut } from '$lib';
|
||||
import Section from './Section.svelte';
|
||||
|
||||
let intValue = $state(0);
|
||||
let floatValue = $state(0.2);
|
||||
let float2Value = $state(0.02);
|
||||
let float3Value = $state(1);
|
||||
let vecValue = $state([0.2, 0.3, 0.4]);
|
||||
const options = ['strawberry', 'raspberry', 'chickpeas'];
|
||||
@@ -16,6 +15,7 @@
|
||||
let detailsOpen = $state(false);
|
||||
|
||||
const themes = [
|
||||
'dark',
|
||||
'light',
|
||||
'solarized',
|
||||
'catppuccin',
|
||||
@@ -31,40 +31,63 @@
|
||||
}
|
||||
document.documentElement.classList.add(`theme-${themes[themeIndex]}`);
|
||||
});
|
||||
|
||||
const colors = [
|
||||
'layer-0',
|
||||
'layer-1',
|
||||
'layer-2',
|
||||
'layer-3',
|
||||
'active',
|
||||
'selected',
|
||||
'outline',
|
||||
'connection',
|
||||
'edge',
|
||||
'text'
|
||||
];
|
||||
</script>
|
||||
|
||||
<main class="flex flex-col gap-8 py-8">
|
||||
<div class="flex gap-4">
|
||||
<h1 class="text-4xl">@nodarium/ui</h1>
|
||||
<Select bind:value={themeIndex} options={themes}></Select>
|
||||
<InputSelect bind:value={themeIndex} options={themes}></InputSelect>
|
||||
</div>
|
||||
|
||||
<Section title="Integer (step inherit)" value={intValue}>
|
||||
<Float bind:value={intValue} max={2} />
|
||||
<Section title="Colors">
|
||||
<table>
|
||||
<tbody>
|
||||
{#each colors as color (color)}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="w-6 h-6 mr-2 my-1 rounded-sm outline-1 bg-{color}"></div>
|
||||
</td>
|
||||
<td>{color}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
|
||||
<Section title="Float (step inherit)" value={floatValue}>
|
||||
<Float bind:value={floatValue} />
|
||||
</Section>
|
||||
|
||||
<Section title="Float 2 (step inherit)" value={intValue}>
|
||||
<Float bind:value={float2Value} />
|
||||
</Section>
|
||||
|
||||
<Section title="Float (0.01 step)" value={floatValue}>
|
||||
<Float bind:value={float3Value} step={0.01} max={3} />
|
||||
<Section title="InputNumber">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>value={floatValue}</p>
|
||||
<InputNumber bind:value={floatValue} />
|
||||
<p>min=0 max=10 step=1 value={intValue}</p>
|
||||
<InputNumber bind:value={intValue} min={0} max={10} step={1} />
|
||||
<p>min=0 max=3 step=0.01 value={float3Value}</p>
|
||||
<InputNumber bind:value={float3Value} step={0.01} max={3} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Vec3" value={JSON.stringify(vecValue)}>
|
||||
<Vec3 bind:value={vecValue} />
|
||||
<InputVec3 bind:value={vecValue} />
|
||||
</Section>
|
||||
|
||||
<Section title="Select" value={d}>
|
||||
<Select bind:value={selectValue} {options} />
|
||||
<InputSelect bind:value={selectValue} {options} />
|
||||
</Section>
|
||||
|
||||
<Section title="Checkbox" value={checked}>
|
||||
<Checkbox bind:value={checked} />
|
||||
<InputCheckbox bind:value={checked} />
|
||||
</Section>
|
||||
|
||||
<Section title="Details" value={detailsOpen}>
|
||||
@@ -74,7 +97,11 @@
|
||||
</Section>
|
||||
|
||||
<Section title="Shortcut">
|
||||
<div class="flex gap-4">
|
||||
<ShortCut ctrl key="S" />
|
||||
<ShortCut alt key="F4" />
|
||||
<ShortCut alt ctrl key="delete" />
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { type Snippet } from 'svelte';
|
||||
let { title, value, children } = $props<{
|
||||
let { title, value, children, class: _class } = $props<{
|
||||
title?: string;
|
||||
value?: unknown;
|
||||
children?: Snippet;
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<section class="border border-1/2 mb-4 p-4 flex flex-col gap-4">
|
||||
<section class="border-outline border-1/2 bg-layer-1 rounded border mb-4 p-4 flex flex-col gap-4 {_class}">
|
||||
<h3 class="flex gap-2 font-bold">
|
||||
{title}
|
||||
<p class="font-normal! opacity-50!">{value}</p>
|
||||
|
||||
@@ -11,7 +11,7 @@ const config = {
|
||||
|
||||
kit: {
|
||||
paths: {
|
||||
base: BASE_URL
|
||||
base: BASE_URL === '/' ? '' : BASE_URL
|
||||
},
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,52 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { exec } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const postDevPackagePlugin = () => {
|
||||
const packageContent = JSON.parse(readFileSync('./package.json', { encoding: 'utf8' }));
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
return {
|
||||
name: 'run-command-on-change',
|
||||
handleHotUpdate: () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(function() {
|
||||
exec(packageContent.scripts.package);
|
||||
}, 200);
|
||||
}
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
plugins: [tailwindcss(), sveltekit(), postDevPackagePlugin()],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
{
|
||||
extends: './vite.config.ts',
|
||||
test: {
|
||||
name: 'client',
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'firefox', headless: true }]
|
||||
},
|
||||
include: ['src/**/*.svelte.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}']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodarium/utils",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,6 @@ test('encode_float', () => {
|
||||
const input = 1.23;
|
||||
const encoded = encodeFloat(input);
|
||||
const output = decodeFloat(encoded);
|
||||
console.log(input, output);
|
||||
expect(output).toBeCloseTo(input);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,24 +21,6 @@ test('fastHashArray doesnt product collisions', () => {
|
||||
expect(hash_a).not.toEqual(hash_b);
|
||||
});
|
||||
|
||||
test('fastHashArray is fast(ish) < 20ms', () => {
|
||||
const a = new Int32Array(10_000);
|
||||
|
||||
const t0 = performance.now();
|
||||
fastHashArrayBuffer(a);
|
||||
|
||||
const t1 = performance.now();
|
||||
|
||||
a[0] = 1;
|
||||
|
||||
fastHashArrayBuffer(a);
|
||||
|
||||
const t2 = performance.now();
|
||||
|
||||
expect(t1 - t0).toBeLessThan(20);
|
||||
expect(t2 - t1).toBeLessThan(20);
|
||||
});
|
||||
|
||||
// test if the fastHashArray function is deterministic
|
||||
test('fastHashArray is deterministic', () => {
|
||||
const a = new Int32Array(1000);
|
||||
|
||||
@@ -8,8 +8,6 @@ test('it correctly concats nested arrays', () => {
|
||||
|
||||
const output = concatEncodedArrays([input_a, input_b, input_c]);
|
||||
|
||||
console.log('Output', output);
|
||||
|
||||
const decoded = decodeNestedArray(output);
|
||||
|
||||
expect(decoded[0]).toEqual([1, 2, 3]);
|
||||
|
||||
@@ -88,7 +88,7 @@ function decode_recursive(dense: number[] | Int32Array, index = 0) {
|
||||
dense,
|
||||
index
|
||||
);
|
||||
decoded.push(...p);
|
||||
decoded.push(p as number[]);
|
||||
index = nextIndex + 1;
|
||||
nextBracketIndex = _nextBracketIndex;
|
||||
} else {
|
||||
|
||||
655
pnpm-lock.yaml
generated
655
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,8 @@ packages:
|
||||
- app
|
||||
- packages/*
|
||||
- nodes/**
|
||||
- '!**/.template/**'
|
||||
- '!**/pkg/**'
|
||||
- "!**/.template/**"
|
||||
- "!**/pkg/**"
|
||||
|
||||
catalog:
|
||||
chokidar-cli: github:open-cli-tools/chokidar-cli#semver:v4.0.0
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- dprint
|
||||
|
||||
Reference in New Issue
Block a user