diff --git a/.agents/skills/open-pr/SKILL.md b/.agents/skills/open-pr/SKILL.md new file mode 100644 index 00000000..ceab5045 --- /dev/null +++ b/.agents/skills/open-pr/SKILL.md @@ -0,0 +1,97 @@ +--- +name: open-pr +description: Open a pull request on pascalorg/editor using the repo's PR template. Use when the user asks to open/create a PR, push and PR, or ship a branch in the editor repo. +allowed-tools: Bash(git *) Bash(gh *) Read +--- + +Open a pull request against `pascalorg/editor` from the current branch. + +## 1. Pre-flight + +```bash +git status # confirm working tree state +git branch --show-current # confirm we're on a feature branch, not main +git log --oneline main..HEAD +``` + +Stop if: +- The current branch is `main`. Ask the user to create a feature branch first. +- The branch has no commits ahead of `main`. Nothing to open a PR for. +- There are uncommitted changes the user hasn't asked to commit. + +Run a build sanity check if the change is non-trivial: + +```bash +bun typecheck +bun build +``` + +Don't open the PR with a broken build. + +## 2. Read the PR template + +The template is at `.github/pull_request_template.md`. Read it before composing the body — the section headings and checklist items are the source of truth, not your memory of them. + +```bash +cat .github/pull_request_template.md +``` + +Mirror the template exactly: + +- `## What does this PR do?` — one paragraph or a short bullet list. Link related issues with `Fixes #123` when applicable. +- `## How to test` — numbered, concrete reviewer steps (commands to run, what to click, expected outcome). +- `## Screenshots / screen recording` — if the change is visual, paste a recording link or note that one will be added. If purely non-visual (refactor, internal API), say so explicitly so the reviewer knows nothing is missing. +- `## Checklist` — copy the boxes verbatim, ticking the ones already verified. + +## 3. Push and open + +```bash +git push -u origin HEAD +``` + +Check for an existing PR first: + +```bash +gh pr view --json url 2>/dev/null +``` + +If none exists, create it. Pass the body via HEREDOC to preserve markdown formatting: + +```bash +gh pr create --title "short, scope-prefixed title" --body "$(cat <<'EOF' +## What does this PR do? + + + +## How to test + +1. +2. +3. + +## Screenshots / screen recording + + + +## Checklist + +- [x] I've tested this locally with `bun dev` +- [x] My code follows the existing code style (run `bun check` to verify) +- [ ] I've updated relevant documentation (if applicable) +- [x] This PR targets the `main` branch +EOF +)" +``` + +Keep the title under ~70 characters. Use a scope prefix when there's an obvious one (`viewer:`, `core:`, `editor:`, `mcp:`). + +If a PR already exists, print its URL and stop — don't recreate. + +## 4. Report + +Return: + +- PR URL +- Title used +- Local typecheck/build status (if you ran them) +- A note for the reviewer if anything in the checklist is left unchecked diff --git a/.claude/skills/review-architecture/SKILL.md b/.agents/skills/review-architecture/SKILL.md similarity index 89% rename from .claude/skills/review-architecture/SKILL.md rename to .agents/skills/review-architecture/SKILL.md index 84cb6a80..a6fbc1c9 100644 --- a/.claude/skills/review-architecture/SKILL.md +++ b/.agents/skills/review-architecture/SKILL.md @@ -10,16 +10,16 @@ Architectural review for Pascal PRs. The user will provide a PR URL, branch name Read these before reviewing any diff. They are the source of truth, not your training data: -- `.claude/rules/systems.md` — core systems vs viewer systems, what each may do -- `.claude/rules/renderers.md` — renderer responsibilities and prohibitions -- `.claude/rules/tools.md` — editor tools live only in `apps/editor/components/tools/` -- `.claude/rules/viewer-isolation.md` — viewer must stay editor-agnostic -- `.claude/rules/layers.md` -- `.claude/rules/selection-managers.md` -- `.claude/rules/scene-registry.md` -- `.claude/rules/spatial-queries.md` -- `.claude/rules/node-schemas.md` -- `.claude/rules/events.md` +- `wiki/architecture/systems.md` — core systems vs viewer systems, what each may do +- `wiki/architecture/renderers.md` — renderer responsibilities and prohibitions +- `wiki/architecture/tools.md` — editor tools live only in `apps/editor/components/tools/` +- `wiki/architecture/viewer-isolation.md` — viewer must stay editor-agnostic +- `wiki/architecture/layers.md` +- `wiki/architecture/selection-managers.md` +- `wiki/architecture/scene-registry.md` +- `wiki/architecture/spatial-queries.md` +- `wiki/architecture/node-schemas.md` +- `wiki/architecture/events.md` Only the first four are required on every review; read the rest when the diff touches their subject area. @@ -73,7 +73,7 @@ Write the classification down before writing findings. If core gains "Floorplan" - `packages/core/**` does not import Three.js, react-three-fiber, or anything from `packages/viewer` / `apps/editor`. - `packages/core/**` does not introduce types or helpers named after an editor view (`Floorplan*`, `Paint*`, `Draft*`). Generic plan-geometry helpers are fine; view-specific vocabulary is not. - Renderers contain no geometry generation or domain logic — that belongs in a system. -- Tools mutate `useScene` (committed state) and `useLiveTransforms` (ephemeral drag state); direct `sceneRegistry` mesh transforms are allowed only under the live-drag exception in `.claude/rules/tools.md`. No business logic, no imports from `packages/viewer`. +- Tools mutate `useScene` (committed state) and `useLiveTransforms` (ephemeral drag state); direct `sceneRegistry` mesh transforms are allowed only under the live-drag exception in `wiki/architecture/tools.md`. No business logic, no imports from `packages/viewer`. ### B. Hook hygiene (`useEditor`, `useScene`, `useViewer`) - Stores hold state + setters only. No business logic, side effects, async work, or derived computations inside the store definition. @@ -95,7 +95,7 @@ Write the classification down before writing findings. If core gains "Floorplan" Group findings by severity: -- **Blocker** — violates a rule in `.claude/rules` or breaks a layer boundary. Must be fixed before merge. +- **Blocker** — violates a rule in `wiki/architecture/` or breaks a layer boundary. Must be fixed before merge. - **Suggestion** — likely problem, worth discussing. Not a hard block. - **Nit** — minor, optional. @@ -103,7 +103,7 @@ For each finding, include: 1. File and line: `path/to/file.ts:42` 2. The offending snippet (short — 1–5 lines) -3. The rule it violates, linked to the rule file (e.g. `.claude/rules/viewer-isolation.md`) +3. The rule it violates, linked to the wiki page (e.g. `wiki/architecture/viewer-isolation.md`) 4. A concrete proposed fix Skip formatting, import ordering, and anything CI already covers. diff --git a/.claude/rules/creating-rules.md b/.claude/rules/creating-rules.md deleted file mode 120000 index 6fb51cc9..00000000 --- a/.claude/rules/creating-rules.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/creating-rules.mdc \ No newline at end of file diff --git a/.claude/rules/events.md b/.claude/rules/events.md deleted file mode 120000 index 72ba2224..00000000 --- a/.claude/rules/events.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/events.mdc \ No newline at end of file diff --git a/.claude/rules/layers.md b/.claude/rules/layers.md deleted file mode 120000 index de51970d..00000000 --- a/.claude/rules/layers.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/layers.mdc \ No newline at end of file diff --git a/.claude/rules/node-schemas.md b/.claude/rules/node-schemas.md deleted file mode 120000 index 93ceffa0..00000000 --- a/.claude/rules/node-schemas.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/node-schemas.mdc \ No newline at end of file diff --git a/.claude/rules/renderers.md b/.claude/rules/renderers.md deleted file mode 120000 index a7bb563a..00000000 --- a/.claude/rules/renderers.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/renderers.mdc \ No newline at end of file diff --git a/.claude/rules/scene-registry.md b/.claude/rules/scene-registry.md deleted file mode 120000 index 017d4c15..00000000 --- a/.claude/rules/scene-registry.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/scene-registry.mdc \ No newline at end of file diff --git a/.claude/rules/selection-managers.md b/.claude/rules/selection-managers.md deleted file mode 120000 index 3c2d1d93..00000000 --- a/.claude/rules/selection-managers.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/selection-managers.mdc \ No newline at end of file diff --git a/.claude/rules/spatial-queries.md b/.claude/rules/spatial-queries.md deleted file mode 120000 index c3345522..00000000 --- a/.claude/rules/spatial-queries.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/spatial-queries.mdc \ No newline at end of file diff --git a/.claude/rules/systems.md b/.claude/rules/systems.md deleted file mode 120000 index b20dbdc3..00000000 --- a/.claude/rules/systems.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/systems.mdc \ No newline at end of file diff --git a/.claude/rules/tools.md b/.claude/rules/tools.md deleted file mode 120000 index 9c7547e5..00000000 --- a/.claude/rules/tools.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/tools.mdc \ No newline at end of file diff --git a/.claude/rules/viewer-isolation.md b/.claude/rules/viewer-isolation.md deleted file mode 120000 index f484584b..00000000 --- a/.claude/rules/viewer-isolation.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/viewer-isolation.mdc \ No newline at end of file diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.codex/rules/creating-rules.md b/.codex/rules/creating-rules.md deleted file mode 120000 index 6fb51cc9..00000000 --- a/.codex/rules/creating-rules.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/creating-rules.mdc \ No newline at end of file diff --git a/.codex/rules/events.md b/.codex/rules/events.md deleted file mode 120000 index 72ba2224..00000000 --- a/.codex/rules/events.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/events.mdc \ No newline at end of file diff --git a/.codex/rules/layers.md b/.codex/rules/layers.md deleted file mode 120000 index de51970d..00000000 --- a/.codex/rules/layers.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/layers.mdc \ No newline at end of file diff --git a/.codex/rules/node-schemas.md b/.codex/rules/node-schemas.md deleted file mode 120000 index 93ceffa0..00000000 --- a/.codex/rules/node-schemas.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/node-schemas.mdc \ No newline at end of file diff --git a/.codex/rules/renderers.md b/.codex/rules/renderers.md deleted file mode 120000 index a7bb563a..00000000 --- a/.codex/rules/renderers.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/renderers.mdc \ No newline at end of file diff --git a/.codex/rules/scene-registry.md b/.codex/rules/scene-registry.md deleted file mode 120000 index 017d4c15..00000000 --- a/.codex/rules/scene-registry.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/scene-registry.mdc \ No newline at end of file diff --git a/.codex/rules/selection-managers.md b/.codex/rules/selection-managers.md deleted file mode 120000 index 3c2d1d93..00000000 --- a/.codex/rules/selection-managers.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/selection-managers.mdc \ No newline at end of file diff --git a/.codex/rules/spatial-queries.md b/.codex/rules/spatial-queries.md deleted file mode 120000 index c3345522..00000000 --- a/.codex/rules/spatial-queries.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/spatial-queries.mdc \ No newline at end of file diff --git a/.codex/rules/systems.md b/.codex/rules/systems.md deleted file mode 120000 index b20dbdc3..00000000 --- a/.codex/rules/systems.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/systems.mdc \ No newline at end of file diff --git a/.codex/rules/tools.md b/.codex/rules/tools.md deleted file mode 120000 index 9c7547e5..00000000 --- a/.codex/rules/tools.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/tools.mdc \ No newline at end of file diff --git a/.codex/rules/viewer-isolation.md b/.codex/rules/viewer-isolation.md deleted file mode 120000 index f484584b..00000000 --- a/.codex/rules/viewer-isolation.md +++ /dev/null @@ -1 +0,0 @@ -../../.cursor/rules/viewer-isolation.mdc \ No newline at end of file diff --git a/.codex/skills b/.codex/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.codex/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.codex/skills/review-architecture/SKILL.md b/.codex/skills/review-architecture/SKILL.md deleted file mode 100644 index 6d3958cd..00000000 --- a/.codex/skills/review-architecture/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: review-architecture -description: Review a PR against the Pascal architectural rules — layer boundaries (core/viewer/editor), systems/renderers/tools separation, hook hygiene (useEditor/useScene/useViewer), and selector performance. Use when the user asks to review a PR, audit a branch, or check that changes respect the codebase's architecture. -allowed-tools: Bash(git *) Bash(gh *) Read Grep Glob ---- - -Architectural review for Pascal PRs. The user will provide a PR URL, branch name, or ask to review the current branch. - -## 1. Load the rules (required — do not skip) - -Read these before reviewing any diff. They are the source of truth, not your training data: - -- `.codex/rules/systems.md` — core systems vs viewer systems, what each may do -- `.codex/rules/renderers.md` — renderer responsibilities and prohibitions -- `.codex/rules/tools.md` — editor tools live only in `apps/editor/components/tools/` -- `.codex/rules/viewer-isolation.md` — viewer must stay editor-agnostic -- `.codex/rules/layers.md` -- `.codex/rules/selection-managers.md` -- `.codex/rules/scene-registry.md` -- `.codex/rules/spatial-queries.md` -- `.codex/rules/node-schemas.md` -- `.codex/rules/events.md` - -Only the first four are required on every review; read the rest when the diff touches their subject area. - -## 2. Fetch the diff - -```bash -# If the user gave a PR URL or number: -gh pr diff - -# If reviewing the current branch: -git diff main...HEAD -``` - -Also list changed files so you can map each to the relevant rule: - -```bash -gh pr view --json files --jq '.files[].path' -# or -git diff --name-only main...HEAD -``` - -## 3. Layer classification — do this BEFORE the checklist - -For every new file, new type, new store field, or new exported helper introduced by the diff, answer one question: **which layer does this belong to — core, viewer, or editor?** If the answer is "editor" but the code lives in `packages/core` or `packages/viewer` (or vice versa), flag it as a **blocker**. This is the most common and most damaging class of violation, and the checklist below won't reliably catch it on its own — do this pass explicitly. - -### The three layers and what they own - -**`packages/core` — domain data + pure logic.** -Owns: node schemas, the scene store (`useScene`), live transforms store, core systems (wall mitering, slab polygons, space detection), event bus, plain 2D/3D math helpers, `sceneRegistry`. Consumed by every downstream package, including read-only embeds. Must not know about: Three.js/R3F, `packages/viewer`, `apps/editor`, any rendering or UI concept, any tool/mode/phase concept, or any *view*-specific concept (floorplan, paint preview, cursor indicators, selection outline styling, etc.). - -**`packages/viewer` — the 3D canvas, shippable standalone.** -Owns: ``, renderers, viewer systems (cutouts, zones, level positions, scans), the viewer store (`useViewer`) *for genuine presentation state only* (selection path, camera/level/wall/view modes, theme, display toggles, hover id). Consumed by both the editor and the read-only `/viewer/[id]` route. Must not know about: editor state (`useEditor`, tools, phases, modes), editor-only names baked into presentation modes (`'delete'`, `'paint-ready'`), editor-only state types (material preview, active paint target, floorplan anything). - -**`apps/editor` (and editor-scoped packages) — the editing experience.** -Owns: tools, `useEditor`, action menus, panels, the floorplan panel and its helpers, paint mode, selection-manager phase/mode logic, cursor badges, command palette, keyboard shortcuts — anything absent from the read-only viewer route. Injects itself into `` via children and props, never the reverse. - -### Five triggers that mean "this is probably editor" - -1. **Would the read-only `/viewer/[id]` route need this?** If no, it belongs in `apps/editor`. -2. **Does the name contain an editor-specific word?** (`Floorplan`, `Paint…`, `Draft…`, `Marquee`, `CursorBadge`, `HoverMode`, `…Tool`, `Moving…`, `Curving…`.) Default to editor and justify loudly if it's anywhere else. -3. **Does the type or field reference a tool/mode/phase vocabulary?** (`'delete'`, `'paint-ready'`, `'material-paint'`, `'site'`/`'structure'`/`'furnish'`, `'build'`/`'edit'`.) Belongs in `useEditor`, not `useViewer` or core. -4. **Does the helper compute something only a 2D editor view needs?** (Floorplan transforms, measurement offsets, SVG path builders, marquee bounds scoped to floorplan.) Editor. Generic 2D geometry that any view could use (polygon math, rotation, clamping, line thickening) can live in core *as long as its names are generic* — no `Floorplan` prefix. -5. **Does a new store field have a setter that no part of the target layer ever calls?** (e.g. `setMaterialPreview` in `useViewer` that only the editor would ever invoke.) That's a layering smell — the state belongs in the caller's layer. - -Write the classification down before writing findings. If core gains "Floorplan" types, or the viewer gains paint-mode vocabulary, or a renderer grows editor awareness — those are the blockers to lead with, not downstream symptoms. - -## 4. Review checklist - -### A. Layer boundaries -- `packages/viewer/**` does not import from `apps/editor` or reference `useEditor`, tool state, phase, or mode. -- `packages/core/**` does not import Three.js, react-three-fiber, or anything from `packages/viewer` / `apps/editor`. -- `packages/core/**` does not introduce types or helpers named after an editor view (`Floorplan*`, `Paint*`, `Draft*`). Generic plan-geometry helpers are fine; view-specific vocabulary is not. -- Renderers contain no geometry generation or domain logic — that belongs in a system. -- Tools mutate `useScene` (committed state) and `useLiveTransforms` (ephemeral drag state); direct `sceneRegistry` mesh transforms are allowed only under the live-drag exception in `.codex/rules/tools.md`. No business logic, no imports from `packages/viewer`. - -### B. Hook hygiene (`useEditor`, `useScene`, `useViewer`) -- Stores hold state + setters only. No business logic, side effects, async work, or derived computations inside the store definition. -- Derived values belong in selectors or systems, not in the store body. -- No cross-store coupling: a store's action should not call another store's actions inside itself. -- New state added to `useViewer` must be presentation-only (selection, camera, level mode, display toggles). Editor-only state (active tool, phase, edit mode, paint preview, floorplan state) goes in `useEditor`. - -### C. Selector performance -- Top-level components (pages, layouts, providers, `` siblings) must not subscribe to large or frequently-changing slices — e.g. `useScene(s => s.nodes)`, `useScene(s => s)`. Flag these: they re-render the whole subtree on every mutation. -- Selectors that return new object or array references each call (e.g. `s => ({ a: s.a, b: s.b })`, `s => s.items.filter(...)`) without a custom equality function (shallow or custom) are re-render hazards. -- Prefer subscribing by ID deep in the tree (one node per renderer) over subscribing to the full collection high up. - -### D. Separation of concerns -- Viewer and core stay unaware of editor-specific concepts (tools, phases, active modes, editor UI state, view-specific helpers). -- Editor-only overlays and systems are injected as children of ``, not added inside the viewer package. -- New node types added correctly: schema → core system (if derived geometry) → viewer renderer → register in `NodeRenderer`. - -## 5. Output format - -Group findings by severity: - -- **Blocker** — violates a rule in `.codex/rules` or breaks a layer boundary. Must be fixed before merge. -- **Suggestion** — likely problem, worth discussing. Not a hard block. -- **Nit** — minor, optional. - -For each finding, include: - -1. File and line: `path/to/file.ts:42` -2. The offending snippet (short — 1–5 lines) -3. The rule it violates, linked to the rule file (e.g. `.codex/rules/viewer-isolation.md`) -4. A concrete proposed fix - -Skip formatting, import ordering, and anything CI already covers. - -If the PR fully complies, say so explicitly — do not invent nits to appear thorough. - -## 6. Final summary - -End with: - -- Blocker count, suggestion count, nit count -- One-sentence verdict: ready to merge / needs changes / needs discussion -- If blockers exist, list the files the author should open first diff --git a/.cursor/rules/creating-rules.mdc b/.cursor/rules/creating-rules.mdc deleted file mode 100644 index 1c3639e6..00000000 --- a/.cursor/rules/creating-rules.mdc +++ /dev/null @@ -1,79 +0,0 @@ ---- -description: How to create and maintain project rules -globs: .cursor/rules/** -alwaysApply: false ---- - -# Creating Rules - -Rules live in two places and are kept in sync via symlinks: - -- `.cursor/rules/.mdc` — source of truth (Cursor format) -- `.claude/rules/.md` — symlink pointing to the cursor file - -## Workflow - -**1. Write the rule in `.cursor/rules/`** - -``` -.cursor/rules/my-rule.mdc -``` - -**2. Create a symlink in `.claude/rules/`** - -```bash -ln -s ../../.cursor/rules/my-rule.mdc .claude/rules/my-rule.md -``` - -The `../../` prefix is required because the symlink lives two levels deep. - -**3. Verify** - -```bash -ls -la .claude/rules/my-rule.md -# → .claude/rules/my-rule.md -> ../../.cursor/rules/my-rule.mdc -``` - -## Rule File Format - -```markdown ---- -description: One-line summary of what this rule covers -globs: -alwaysApply: false ---- - -# Rule Title - -Short intro paragraph. - -## Section - -Concrete guidance with examples. -``` - -- Set `alwaysApply: true` only for rules that apply to every file in the project. -- Use `globs` to scope a rule to specific paths (e.g. `packages/viewer/**`). - -## Good Practices - -- Keep rules under 500 lines. Split large rules into smaller focused files. -- Include concrete examples or reference real files with `@filename`. -- Add a rule when the same mistake has been made more than once — not preemptively. -- Prefer showing a correct example over listing prohibitions. - -## Existing Rules - -| Rule | Covers | -|---|---| -| `creating-rules` | This file — how to add rules | -| `renderers` | Node renderer pattern in `packages/viewer` | -| `systems` | Core and viewer systems architecture | -| `tools` | Editor tools structure in `apps/editor` | -| `viewer-isolation` | Keeping `@pascal-app/viewer` editor-agnostic | -| `scene-registry` | Global node ID → Object3D map and `useRegistry` | -| `selection-managers` | Two-layer selection (viewer + editor), events, outliner | -| `events` | Typed event bus — emitting and listening to node and grid events | -| `node-schemas` | Zod schema pattern for node types, createNode, updateNode | -| `spatial-queries` | Placement validation (canPlaceOnFloor/Wall/Ceiling) for tools | -| `layers` | Three.js layer constants, ownership, and rendering separation | diff --git a/.cursor/skills b/.cursor/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.cursor/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.cursor/skills/review-architecture/SKILL.md b/.cursor/skills/review-architecture/SKILL.md deleted file mode 120000 index 0fb1d1aa..00000000 --- a/.cursor/skills/review-architecture/SKILL.md +++ /dev/null @@ -1 +0,0 @@ -../../../.claude/skills/review-architecture/SKILL.md \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce8cff52..e539ef5f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,9 @@ on: options: - core - viewer - - both + - editor + - mcp + - all bump: description: "Version bump" required: true @@ -51,56 +53,115 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Bump & publish core - if: inputs.package == 'core' || inputs.package == 'both' + - name: Bump versions and sync inter-package references + run: | + BUMP=${{ inputs.bump }} + TARGET=${{ inputs.package }} + + bump_version() { + local v=$1 + IFS='.' read -r MAJ MIN PAT <<< "$v" + if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi + if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi + if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi + echo "$MAJ.$MIN.$PAT" + } + + # Track new versions in shell-local vars. + # NOTE: $GITHUB_ENV writes don't surface within the same step, so the + # peerDeps sync below must use shell vars, not env indirection. + declare -A NEW_VERSIONS + + for pkg in core viewer editor mcp; do + if [ "$TARGET" = "$pkg" ] || [ "$TARGET" = "all" ]; then + CUR=$(jq -r '.version' packages/$pkg/package.json) + NEW=$(bump_version "$CUR") + jq --arg v "$NEW" '.version = $v' packages/$pkg/package.json > tmp.json && mv tmp.json packages/$pkg/package.json + NEW_VERSIONS[$pkg]=$NEW + UPPER=$(echo "$pkg" | tr '[:lower:]' '[:upper:]') + # Also export for the publish/commit/tag steps that follow. + echo "${UPPER}_VERSION=$NEW" >> $GITHUB_ENV + echo "Bumped @pascal-app/$pkg: $CUR → $NEW" + fi + done + + # Sync inter-package references in peerDependencies and devDependencies. + # Anything that references a bumped @pascal-app/* package is updated to ^NEW. + for pkg in core viewer editor mcp; do + FILE=packages/$pkg/package.json + for dep in core viewer editor mcp; do + VAL="${NEW_VERSIONS[$dep]}" + [ -z "$VAL" ] && continue + jq --arg name "@pascal-app/$dep" --arg v "^$VAL" ' + if .peerDependencies[$name] then .peerDependencies[$name] = $v else . end + | if .devDependencies[$name] then .devDependencies[$name] = $v else . end + ' "$FILE" > tmp.json && mv tmp.json "$FILE" + done + done + + echo "=== @pascal-app/* refs after sync ===" + for pkg in core viewer editor mcp; do + echo "--- packages/$pkg/package.json ---" + jq '{ peerDependencies: (.peerDependencies // {} | with_entries(select(.key | startswith("@pascal-app/")))), devDependencies: (.devDependencies // {} | with_entries(select(.key | startswith("@pascal-app/")))) }' packages/$pkg/package.json + done + + - name: Build & publish core + if: inputs.package == 'core' || inputs.package == 'all' working-directory: packages/core env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - BUMP=${{ inputs.bump }} - VERSION=$(jq -r '.version' package.json) - IFS='.' read -r MAJ MIN PAT <<< "$VERSION" - if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi - if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi - if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi - VERSION="$MAJ.$MIN.$PAT" - jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json - echo "CORE_VERSION=$VERSION" >> $GITHUB_ENV - bun run build - if [ "${{ inputs.dry-run }}" = "true" ]; then - echo "🏜️ Dry run — would publish @pascal-app/core@$VERSION" + echo "🏜️ Dry run — would publish @pascal-app/core@$CORE_VERSION" npm publish --dry-run --access public else npm publish --access public - echo "📦 Published @pascal-app/core@$VERSION" + echo "📦 Published @pascal-app/core@$CORE_VERSION" fi - - name: Bump & publish viewer - if: inputs.package == 'viewer' || inputs.package == 'both' + - name: Build & publish viewer + if: inputs.package == 'viewer' || inputs.package == 'all' working-directory: packages/viewer env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - BUMP=${{ inputs.bump }} - VERSION=$(jq -r '.version' package.json) - IFS='.' read -r MAJ MIN PAT <<< "$VERSION" - if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi - if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi - if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi - VERSION="$MAJ.$MIN.$PAT" - jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json - echo "VIEWER_VERSION=$VERSION" >> $GITHUB_ENV - bun run build - if [ "${{ inputs.dry-run }}" = "true" ]; then - echo "🏜️ Dry run — would publish @pascal-app/viewer@$VERSION" + echo "🏜️ Dry run — would publish @pascal-app/viewer@$VIEWER_VERSION" npm publish --dry-run --access public else npm publish --access public - echo "📦 Published @pascal-app/viewer@$VERSION" + echo "📦 Published @pascal-app/viewer@$VIEWER_VERSION" + fi + + - name: Publish editor + if: inputs.package == 'editor' || inputs.package == 'all' + working-directory: packages/editor + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ "${{ inputs.dry-run }}" = "true" ]; then + echo "🏜️ Dry run — would publish @pascal-app/editor@$EDITOR_VERSION" + npm publish --dry-run --access public + else + npm publish --access public + echo "📦 Published @pascal-app/editor@$EDITOR_VERSION" + fi + + - name: Build & publish mcp + if: inputs.package == 'mcp' || inputs.package == 'all' + working-directory: packages/mcp + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + bun run build + if [ "${{ inputs.dry-run }}" = "true" ]; then + echo "🏜️ Dry run — would publish @pascal-app/mcp@$MCP_VERSION" + npm publish --dry-run --access public + else + npm publish --access public + echo "📦 Published @pascal-app/mcp@$MCP_VERSION" fi - name: Commit version bumps & tag @@ -118,6 +179,14 @@ jobs: PKGS="$PKGS @pascal-app/viewer@$VIEWER_VERSION" TAGS="$TAGS @pascal-app/viewer@$VIEWER_VERSION" fi + if [ -n "$EDITOR_VERSION" ]; then + PKGS="$PKGS @pascal-app/editor@$EDITOR_VERSION" + TAGS="$TAGS @pascal-app/editor@$EDITOR_VERSION" + fi + if [ -n "$MCP_VERSION" ]; then + PKGS="$PKGS @pascal-app/mcp@$MCP_VERSION" + TAGS="$TAGS @pascal-app/mcp@$MCP_VERSION" + fi git commit -m "release:${PKGS}" diff --git a/AGENTS.md b/AGENTS.md index c0467aa5..d55de298 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,49 +1,51 @@ -# Pascal Agent Instructions +# Agent Instructions — `pascalorg/editor` -This repository uses shared architecture rules for AI assistants. Treat the rule files as the source of truth for architecture-sensitive work. +Public, open-source home of `@pascal-app/{core,viewer,editor,mcp}` and the standalone editor app. Consumed both as npm packages and (in `pascalorg/private-editor`) as a git submodule. -## Required Rule Sources +## Repo Shape -The canonical rules live in `.cursor/rules/*.mdc`. +| Path | Purpose | +|---|---| +| `packages/core` | Scene graph, node schemas, stores, event bus, core systems — pure logic, no Three.js | +| `packages/viewer` | Standalone 3D canvas: renderers, viewer systems, presentation state | +| `packages/editor` | Editor UI components reused by the standalone app and embedders | +| `packages/mcp` | MCP server and scene storage adapters | +| `apps/editor` | Standalone editor app — composes `viewer` + `editor` + tools | -Claude-compatible paths are exposed in `.claude/rules/*.md`. -Codex-compatible paths are exposed in `.codex/rules/*.md`. +## Where to look -Both should point to the same Cursor rule sources so Claude and Codex review the exact same rules. +- **Architecture rules** — `wiki/architecture/` (read on demand; index in `wiki/architecture/README.md`). +- **Skills (ready workflows)** — `.agents/skills//SKILL.md`. Same content is reachable as `.claude/skills/`, `.cursor/skills/`, `.codex/skills/` (symlinks to `.agents/skills/`). +- **Repo orientation for humans** — `README.md`, `SETUP.md`, `CONTRIBUTING.md`. -## Architecture Rules +`CLAUDE.md`, `GEMINI.md`, and `.github/copilot-instructions.md` are symlinks to this file. Codex reads this file directly. -Read the relevant rules before making or reviewing changes in these areas: +## Layer Boundaries (read once, internalise) -- `.codex/rules/systems.md` — core systems vs viewer systems, what each may do -- `.codex/rules/renderers.md` — renderer responsibilities and prohibitions -- `.codex/rules/tools.md` — editor tools live only in `apps/editor/components/tools/` -- `.codex/rules/viewer-isolation.md` — viewer must stay editor-agnostic -- `.codex/rules/layers.md` -- `.codex/rules/selection-managers.md` -- `.codex/rules/scene-registry.md` -- `.codex/rules/spatial-queries.md` -- `.codex/rules/node-schemas.md` -- `.codex/rules/events.md` +- **`packages/core`** owns domain data and pure logic. It must not import Three.js, `packages/viewer`, `apps/editor`, rendering/UI concepts, tools, modes, phases, or view-specific concepts such as floorplan or paint preview. +- **`packages/viewer`** owns the standalone 3D canvas, renderers, viewer systems, and genuine presentation state. It must not know about `useEditor`, editor tools, phases, modes, paint mode, floorplan state, or editor-only presentation vocabulary. +- **`apps/editor`** owns the editing experience: tools, `useEditor`, panels, floorplan helpers, paint mode, keyboard shortcuts, command palette, action menus, cursor badges, and editor-only overlays. Editor features are injected into `` via props and children. -For architecture reviews, the first four are always required. Read the remaining rules when the diff touches their subject area. +Details, examples, and rationale live in `wiki/architecture/layers.md`, `wiki/architecture/viewer-isolation.md`, `wiki/architecture/systems.md`, `wiki/architecture/renderers.md`, `wiki/architecture/tools.md`. -## Layer Boundaries +## When making architecture-sensitive changes -`packages/core` owns domain data and pure logic. It must not import Three.js, `packages/viewer`, `apps/editor`, rendering/UI concepts, tools, modes, phases, or view-specific concepts such as floorplan or paint preview. +Read the relevant page in `wiki/architecture/` **before** writing code. The page list lives in `wiki/architecture/README.md`. As a minimum: -`packages/viewer` owns the standalone 3D canvas, renderers, viewer systems, and genuine presentation state. It must not know about `useEditor`, editor tools, phases, modes, paint mode, floorplan state, or editor-only presentation vocabulary. +- Adding a node type → `node-schemas.md`, `renderers.md`, `systems.md` +- Adding a tool → `tools.md`, `spatial-queries.md`, `events.md` +- Adding a system → `systems.md`, `scene-registry.md` +- Anything in `packages/viewer` → `viewer-isolation.md`, `layers.md` +- Anything touching selection → `selection-managers.md`, `scene-registry.md`, `events.md` -`apps/editor` owns the editing experience: tools, `useEditor`, panels, floorplan helpers, paint mode, keyboard shortcuts, command palette, action menus, cursor badges, and editor-only overlays. Editor features are injected into `` via props and children. +## When reviewing a PR -## Review Expectations +Invoke the `review-architecture` skill (`.agents/skills/review-architecture/SKILL.md`). It loads the required architecture pages, fetches the diff, classifies each new file by layer, and reports findings grouped by severity. -When reviewing architecture changes: +## Operating rules -1. Classify every new file, type, store field, and exported helper as core, viewer, or editor before writing findings. -2. Lead with layer-boundary blockers. -3. Check hook hygiene for `useEditor`, `useScene`, and `useViewer`. -4. Check selector performance for broad subscriptions and selectors that allocate fresh references. -5. Skip formatting and import ordering unless they hide a real behavior or architecture issue. - -Use `.codex/skills/review-architecture/SKILL.md` when the user asks Codex to review a PR, audit a branch, or check architecture compliance. +- Read the full file before editing. Plan all changes, then make one complete edit. +- When the user corrects you, stop and re-read their message. +- After two consecutive tool failures, stop and change approach. +- Don't introduce backwards-compatibility shims, dead code, or speculative abstractions. +- Don't write new comments unless they explain a non-obvious *why*. diff --git a/.claude/CLAUDE.md b/GEMINI.md similarity index 100% rename from .claude/CLAUDE.md rename to GEMINI.md diff --git a/README.md b/README.md index 1abe0300..1d237bcf 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,7 @@ npm publish --workspace=@pascal-app/viewer --access public Aymeric Rabot Wassim Samad +Sudhir --- diff --git a/apps/editor/app/api/scenes/[id]/route.ts b/apps/editor/app/api/scenes/[id]/route.ts index 3c93f020..1712ad4a 100644 --- a/apps/editor/app/api/scenes/[id]/route.ts +++ b/apps/editor/app/api/scenes/[id]/route.ts @@ -173,7 +173,7 @@ function parseIfMatch(raw: string | null): number | undefined { const inner = match ? match[1] : trimmed if (!inner) return undefined const n = Number(inner) - if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) return undefined + if (!(Number.isFinite(n) && Number.isInteger(n)) || n < 0) return undefined return n } diff --git a/apps/editor/app/globals.css b/apps/editor/app/globals.css index 78ca5812..390aa6e4 100644 --- a/apps/editor/app/globals.css +++ b/apps/editor/app/globals.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import "../../../styles/elevation.css"; @source "../../../packages/editor/src"; @custom-variant dark (&:is(.dark *)); diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx index 72a45c41..80fb1a96 100644 --- a/apps/editor/app/page.tsx +++ b/apps/editor/app/page.tsx @@ -1,13 +1,34 @@ 'use client' -import { Editor, type SidebarTab, ViewerToolbarLeft, ViewerToolbarRight } from '@pascal-app/editor' +import { Editor, ItemsPanel } from '@pascal-app/editor' +import { Layers, Package, Settings } from 'lucide-react' import Link from 'next/link' +import { + CommunityViewerToolbarLeft, + CommunityViewerToolbarRight, +} from '@/components/viewer-toolbar' -const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [ +const SIDEBAR_TABS = [ { id: 'site', label: 'Scene', - component: () => null, // Built-in SitePanel handles this + component: () => null, + mobileDefaultSnap: 0.5, + mobileIcon: , + }, + { + id: 'items', + label: 'Items', + component: ItemsPanel, + mobileDefaultSnap: 0.5, + mobileIcon: , + }, + { + id: 'settings', + label: 'Settings', + component: () => null, + mobileDefaultSnap: 0.5, + mobileIcon: , }, ] @@ -36,8 +57,8 @@ export default function Home() { layoutVersion="v2" projectId={PROJECT_ID} sidebarTabs={SIDEBAR_TABS} - viewerToolbarLeft={} - viewerToolbarRight={} + viewerToolbarLeft={} + viewerToolbarRight={} /> ) diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index 5546c032..b3606f76 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -5,12 +5,11 @@ import { Editor, type SceneGraph, type SidebarTab, - ViewerToolbarLeft, - ViewerToolbarRight, } from '@pascal-app/editor' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' +import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar' export interface SceneMeta { id: string @@ -200,8 +199,8 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { onThumbnailCapture={handleThumb} projectId={meta.projectId ?? 'default'} sidebarTabs={SIDEBAR_TABS} - viewerToolbarLeft={} - viewerToolbarRight={} + viewerToolbarLeft={} + viewerToolbarRight={} /> ) diff --git a/apps/editor/components/toolbar-tooltip.tsx b/apps/editor/components/toolbar-tooltip.tsx new file mode 100644 index 00000000..f3554d01 --- /dev/null +++ b/apps/editor/components/toolbar-tooltip.tsx @@ -0,0 +1,49 @@ +'use client' + +import * as TooltipPrimitive from '@radix-ui/react-tooltip' +import type * as React from 'react' +import { cn } from '@/lib/utils' + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return +} + +function Tooltip({ ...props }: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ ...props }: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 6, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } diff --git a/apps/editor/components/viewer-toolbar.tsx b/apps/editor/components/viewer-toolbar.tsx new file mode 100644 index 00000000..e3668ce2 --- /dev/null +++ b/apps/editor/components/viewer-toolbar.tsx @@ -0,0 +1,362 @@ +'use client' + +import { Icon as IconifyIcon } from '@iconify/react' +import { useEditor, useSidebarStore, type ViewMode } from '@pascal-app/editor' +import { useViewer } from '@pascal-app/viewer' +import { + ChevronsLeft, + ChevronsRight, + Columns2, + Eye, + EyeOff, + Footprints, + Grid2X2, + Moon, + Sun, +} from 'lucide-react' +import Image from 'next/image' +import { type ReactNode, useCallback } from 'react' +import { cn } from '@/lib/utils' +import { Tooltip, TooltipContent, TooltipTrigger } from './toolbar-tooltip' + +const TOOLBAR_CONTAINER = + 'inline-flex h-8 items-stretch overflow-hidden rounded-xl border border-border bg-background/90 shadow-2xl backdrop-blur-md' + +const TOOLBAR_BTN = + 'flex w-8 items-center justify-center text-muted-foreground/80 transition-colors hover:bg-white/8 hover:text-foreground/90' + +function ToolbarTooltip({ children, label }: { children: ReactNode; label: string }) { + return ( + + {children} + {label} + + ) +} + +const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [ + { + id: '3d', + label: '3D', + icon: ( + + ), + }, + { + id: '2d', + label: '2D', + icon: ( + + ), + }, + { + id: 'split', + label: 'Split', + icon: , + }, +] + +const levelModeOrder = ['stacked', 'exploded', 'solo'] as const +const levelModeLabels: Record = { + manual: 'Stack', + stacked: 'Stack', + exploded: 'Exploded', + solo: 'Solo', +} + +const wallModeOrder = ['cutaway', 'up', 'down'] as const +const wallModeConfig: Record = { + up: { icon: '/icons/room.png', label: 'Full height' }, + cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' }, + down: { icon: '/icons/walllow.png', label: 'Low' }, +} + +function ViewModeControl() { + const viewMode = useEditor((state) => state.viewMode) + const setViewMode = useEditor((state) => state.setViewMode) + + return ( +
+ {VIEW_MODES.map((mode) => { + const isActive = viewMode === mode.id + return ( + + + + ) + })} +
+ ) +} + +function CollapseSidebarButton() { + const isCollapsed = useSidebarStore((state) => state.isCollapsed) + const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed) + + const toggle = useCallback(() => { + setIsCollapsed(!isCollapsed) + }, [isCollapsed, setIsCollapsed]) + + return ( +
+ + + +
+ ) +} + +function LevelModeToggle() { + const levelMode = useViewer((state) => state.levelMode) + const setLevelMode = useViewer((state) => state.setLevelMode) + const isDefault = levelMode === 'stacked' || levelMode === 'manual' + + const cycle = () => { + if (levelMode === 'manual') { + setLevelMode('stacked') + return + } + + const index = levelModeOrder.indexOf(levelMode as (typeof levelModeOrder)[number]) + const next = levelModeOrder[(index + 1) % levelModeOrder.length] + if (next) setLevelMode(next) + } + + const label = `Levels: ${levelMode === 'manual' ? 'Manual' : (levelModeLabels[levelMode] ?? 'Stack')}` + + return ( + + + + ) +} + +function WallModeToggle() { + const wallMode = useViewer((state) => state.wallMode) + const setWallMode = useViewer((state) => state.setWallMode) + const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway! + + const cycle = () => { + const index = wallModeOrder.indexOf(wallMode as (typeof wallModeOrder)[number]) + const next = wallModeOrder[(index + 1) % wallModeOrder.length] + if (next) setWallMode(next) + } + + return ( + + + + ) +} + +function GridVisibilityToggle() { + const showGrid = useViewer((state) => state.showGrid) + const setShowGrid = useViewer((state) => state.setShowGrid) + + return ( + + + + ) +} + +function UnitToggle() { + const unit = useViewer((state) => state.unit) + const setUnit = useViewer((state) => state.setUnit) + + return ( + + + + ) +} + +function ThemeToggle() { + const theme = useViewer((state) => state.theme) + const setTheme = useViewer((state) => state.setTheme) + + return ( + + + + ) +} + +function CameraModeToggle() { + const cameraMode = useViewer((state) => state.cameraMode) + const setCameraMode = useViewer((state) => state.setCameraMode) + + return ( + + + + ) +} + +function WalkthroughButton() { + const isFirstPersonMode = useEditor((state) => state.isFirstPersonMode) + const setFirstPersonMode = useEditor((state) => state.setFirstPersonMode) + + return ( + + + + ) +} + +function PreviewButton() { + return ( + + + + ) +} + +export function CommunityViewerToolbarLeft() { + return ( + <> + + + + ) +} + +export function CommunityViewerToolbarRight() { + return ( +
+ + + +
+ + + +
+ + +
+ ) +} diff --git a/apps/editor/env.mjs b/apps/editor/env.mjs index 141ce000..64f1d9d3 100644 --- a/apps/editor/env.mjs +++ b/apps/editor/env.mjs @@ -1,8 +1,8 @@ /** * Environment variable validation for the editor app. * - * This file validates that required environment variables are set at runtime. - * Variables are defined in the root .env file. + * This file validates environment variables used by the standalone editor app. + * Values are loaded from the repo root .env.local by package scripts. * * @see https://env.t3.gg/docs/nextjs */ @@ -13,42 +13,21 @@ export const env = createEnv({ /** * Server-side environment variables (not exposed to client) */ - server: { - // Database - POSTGRES_URL: z.string().min(1), - SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), - - // Auth - BETTER_AUTH_SECRET: z.string().min(1), - GOOGLE_CLIENT_ID: z.string().optional(), - GOOGLE_CLIENT_SECRET: z.string().optional(), - - // Email - RESEND_API_KEY: z.string().optional(), - }, + server: {}, /** * Client-side environment variables (exposed to browser via NEXT_PUBLIC_) */ client: { - NEXT_PUBLIC_SUPABASE_URL: z.string().min(1), - NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().optional(), + NEXT_PUBLIC_ASSETS_CDN_URL: z.string().optional(), }, /** * Runtime values - pulls from process.env */ runtimeEnv: { - // Server - POSTGRES_URL: process.env.POSTGRES_URL, - SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY, - BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - RESEND_API_KEY: process.env.RESEND_API_KEY, - // Client - NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, - NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, + NEXT_PUBLIC_ASSETS_CDN_URL: + process.env.NEXT_PUBLIC_ASSETS_CDN_URL ?? process.env.NEXT_PUBLIC_EDITOR_ASSETS_CDN_URL, }, /** diff --git a/apps/editor/next.config.ts b/apps/editor/next.config.ts index 6c684f1b..0120eaf4 100644 --- a/apps/editor/next.config.ts +++ b/apps/editor/next.config.ts @@ -1,6 +1,9 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { + logging: { + browserToTerminal: true, + }, typescript: { ignoreBuildErrors: true, }, diff --git a/apps/editor/package.json b/apps/editor/package.json index e8b71334..f2c00a2e 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -4,23 +4,26 @@ "type": "module", "private": true, "scripts": { - "dev": "dotenv -e ./.env.local --override -- next dev --port 3002", - "build": "dotenv -e ./.env.local --override -- next build", + "dev": "dotenv -e ../../.env.local -- next dev --port 3002", + "build": "dotenv -e ../../.env.local -- next build", "start": "next start", "lint": "biome lint", "check-types": "next typegen && tsc --noEmit" }, "dependencies": { + "@iconify/react": "^6.0.2", "@number-flow/react": "^0.5.14", "@pascal-app/core": "*", "@pascal-app/editor": "*", "@pascal-app/mcp": "*", "@pascal-app/viewer": "*", + "@radix-ui/react-tooltip": "^1.2.8", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@tailwindcss/postcss": "^4.2.1", "clsx": "^2.1.1", "geist": "^1.7.0", + "lucide-react": "^1.7.0", "next": "16.2.1", "postcss": "^8.5.6", "react": "^19.2.4", @@ -37,9 +40,9 @@ "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "agentation": "^2.3.2", - "react-grab": "^0.1.25", + "react-grab": "^0.1.29", "react-scan": "^0.5.3", "tw-animate-css": "^1.4.0", - "typescript": "5.9.3" + "typescript": "6.0.2" } } diff --git a/apps/editor/public/audios/sfx/snapshot_capture.mp3 b/apps/editor/public/audios/sfx/snapshot_capture.mp3 new file mode 100644 index 00000000..f2cc2d9d Binary files /dev/null and b/apps/editor/public/audios/sfx/snapshot_capture.mp3 differ diff --git a/apps/editor/public/icons/spawn-point.svg b/apps/editor/public/icons/spawn-point.svg new file mode 100644 index 00000000..1e48cde5 --- /dev/null +++ b/apps/editor/public/icons/spawn-point.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/editor/vercel.json b/apps/editor/vercel.json new file mode 100644 index 00000000..45691b1d --- /dev/null +++ b/apps/editor/vercel.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "cd ../.. && npx -y bun@1.3.13 run build --filter=editor", + "installCommand": "cd ../.. && npx -y bun@1.3.13 install --frozen-lockfile", + "outputDirectory": ".next", + "cleanUrls": true, + "trailingSlash": false, + "bunVersion": "1.x" +} diff --git a/bun.lock b/bun.lock index 6bbf8bb8..64ed55c6 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,6 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { "name": "editor", @@ -8,24 +8,37 @@ "@biomejs/biome": "^2.4.6", "dotenv-cli": "^11.0.0", "turbo": "^2.8.15", - "typescript": "5.9.3", + "typescript": "6.0.2", "ultracite": "^7.2.5", }, + "optionalDependencies": { + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + }, }, "apps/editor": { "name": "editor", "version": "0.1.0", "dependencies": { + "@iconify/react": "^6.0.2", "@number-flow/react": "^0.5.14", "@pascal-app/core": "*", "@pascal-app/editor": "*", "@pascal-app/mcp": "*", "@pascal-app/viewer": "*", + "@radix-ui/react-tooltip": "^1.2.8", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@tailwindcss/postcss": "^4.2.1", "clsx": "^2.1.1", "geist": "^1.7.0", + "lucide-react": "^1.7.0", "next": "16.2.1", "postcss": "^8.5.6", "react": "^19.2.4", @@ -42,15 +55,15 @@ "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "agentation": "^2.3.2", - "react-grab": "^0.1.25", + "react-grab": "^0.1.29", "react-scan": "^0.5.3", "tw-animate-css": "^1.4.0", - "typescript": "5.9.3", + "typescript": "6.0.2", }, }, "packages/core": { "name": "@pascal-app/core", - "version": "0.6.0", + "version": "0.8.0", "dependencies": { "dedent": "^1.7.1", "idb-keyval": "^6.2.2", @@ -62,9 +75,10 @@ }, "devDependencies": { "@pascal/typescript-config": "*", + "@types/bun": "^1.3.0", "@types/react": "^19.2.2", "@types/three": "^0.184.0", - "typescript": "5.9.3", + "typescript": "6.0.2", }, "peerDependencies": { "@react-three/drei": "^10", @@ -75,7 +89,7 @@ }, "packages/editor": { "name": "@pascal-app/editor", - "version": "0.6.0", + "version": "0.8.0", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -100,29 +114,29 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "howler": "^2.2.4", - "lucide-react": "^0.562.0", + "lucide-react": "^1.7.0", "mitt": "^3.0.1", "motion": "^12.34.3", "nanoid": "^5.1.6", "tailwind-merge": "^3.5.0", - "three-mesh-bvh": "^0.9.8", + "three-mesh-bvh": "~0.9.8", "zod": "^4.3.6", "zustand": "^5.0.11", }, "devDependencies": { - "@pascal-app/core": "^0.6.0", - "@pascal-app/viewer": "^0.6.0", + "@pascal-app/core": "^0.8.0", + "@pascal-app/viewer": "^0.8.0", "@pascal/typescript-config": "*", + "@types/bun": "^1.3.0", "@types/howler": "^2.2.12", - "@types/node": "^22.19.12", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "@types/three": "^0.184.0", - "typescript": "5.9.3", + "typescript": "6.0.2", }, "peerDependencies": { - "@pascal-app/core": "^0.6.0", - "@pascal-app/viewer": "^0.6.0", + "@pascal-app/core": "^0.8.0", + "@pascal-app/viewer": "^0.8.0", "@react-three/drei": "^10", "@react-three/fiber": "^9", "next": ">=15", @@ -150,7 +164,7 @@ }, "packages/mcp": { "name": "@pascal-app/mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "pascal-mcp": "./dist/bin/pascal-mcp.js", }, @@ -159,13 +173,13 @@ "zod": "^4.3.5", }, "devDependencies": { - "@pascal-app/core": "workspace:*", + "@pascal-app/core": "^0.8.0", "@pascal/typescript-config": "*", "@types/node": "^25.5.0", "typescript": "5.9.3", }, "peerDependencies": { - "@pascal-app/core": "workspace:*", + "@pascal-app/core": "^0.8.0", }, }, "packages/typescript-config": { @@ -191,7 +205,7 @@ }, "packages/viewer": { "name": "@pascal-app/viewer", - "version": "0.6.0", + "version": "0.8.0", "dependencies": { "three-bvh-csg": "^0.0.18", "three-mesh-bvh": "^0.9.8", @@ -199,13 +213,13 @@ }, "devDependencies": { "@pascal/typescript-config": "*", - "@types/node": "^25.5.0", + "@types/node": "^22", "@types/react": "^19.2.2", "@types/three": "^0.184.0", - "typescript": "5.9.3", + "typescript": "6.0.2", }, "peerDependencies": { - "@pascal-app/core": "^0.6.0", + "@pascal-app/core": "^0.8.0", "@react-three/drei": "^10", "@react-three/fiber": "^9", "react": "^18 || ^19", @@ -217,14 +231,19 @@ "version": "0.0.0", }, }, + "overrides": { + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@types/three": "0.184.0", + }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@antfu/ni": ["@antfu/ni@0.23.2", "", { "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nu": "bin/nu.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs" } }, "sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ=="], + "@antfu/ni": ["@antfu/ni@30.1.0", "", { "dependencies": { "fzf": "^0.5.2", "package-manager-detector": "^1.6.0", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15" }, "bin": { "ni": "bin/ni.mjs", "nci": "bin/nci.mjs", "nr": "bin/nr.mjs", "nup": "bin/nup.mjs", "nd": "bin/nd.mjs", "nlx": "bin/nlx.mjs", "na": "bin/na.mjs", "nun": "bin/nun.mjs" } }, "sha512-3VuAbPjgY52rQNn4wABaXMhBU2Oq91uy6L8nX49eJ35OLI68CyckGU+HZxcaHix4ymuGM2nFL1D6sLpgODK5xw=="], "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], @@ -246,7 +265,7 @@ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], @@ -256,27 +275,27 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@biomejs/biome": ["@biomejs/biome@2.4.9", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.9", "@biomejs/cli-darwin-x64": "2.4.9", "@biomejs/cli-linux-arm64": "2.4.9", "@biomejs/cli-linux-arm64-musl": "2.4.9", "@biomejs/cli-linux-x64": "2.4.9", "@biomejs/cli-linux-x64-musl": "2.4.9", "@biomejs/cli-win32-arm64": "2.4.9", "@biomejs/cli-win32-x64": "2.4.9" }, "bin": { "biome": "bin/biome" } }, "sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA=="], + "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.9", "", { "os": "win32", "cpu": "x64" }, "sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], - "@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="], + "@clack/core": ["@clack/core@1.3.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA=="], - "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], + "@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="], "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], @@ -288,59 +307,7 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -370,9 +337,11 @@ "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], @@ -444,15 +413,13 @@ "@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="], - "@medv/finder": ["@medv/finder@4.0.2", "", {}, "sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="], "@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="], - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.14", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-ogBjgsFrPPz19abP3VwcYSahbkUOMMvJjxCOYWYndw+PydeMuLuB4XrvNkNutFrTjC9St2KFULRdKID8Sd/CMQ=="], + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.18", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-w4MYq8M26a8PNrfto0JosLf5/3ssln1rsyP96g2DkC8uFVymStM5DLSz5ElxxrPRg2XnTMnFo3kREFlhYvxhWw=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q=="], @@ -488,19 +455,19 @@ "@pascal/typescript-config": ["@pascal/typescript-config@workspace:tooling/typescript"], - "@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.64", "", {}, "sha512-fyDNvCIGUTUYxDOzQnLOAGEuDQbyjx1JUlEYrjHB8KLDWzVl0j2SoFiLN4AwahEh4pXiA7XdJBNneQKnfMa/Bw=="], + "@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.67", "", {}, "sha512-tBEK+suALkhZ5i9rJBR0PYx4uvDD0nVQGoEENBMLTmeL1kNzhbJ7+mfAv1ATW1w42TOuREI9FfAa5ca1N8pcxQ=="], "@pmndrs/pointer-events": ["@pmndrs/pointer-events@6.6.29", "", {}, "sha512-o4YD6VfJgDYjFgde/YyAw2X5KY454tdmOXrHGOvKTWJBHzkL90B5vH4rqmexwRVvaDfT3YLvVh/Dm5cBbgZXMg=="], - "@pmndrs/uikit": ["@pmndrs/uikit@1.0.64", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.64", "@pmndrs/uikit-pub-sub": "^1.0.64", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-FQgF8sf46U6fmHnqOn32S04lnWiXVxQO6MatL7FgoLKJ74YIBgEHgwKHRwltgLrN/rcTu7LgJm7LgvXvMccEGA=="], + "@pmndrs/uikit": ["@pmndrs/uikit@1.0.67", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.67", "@pmndrs/uikit-pub-sub": "^1.0.67", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-36zaHVHxRPSWgr78r1GUEP0wcfC2hvx1a5t459Yx9IOYqolKCCloCticoSDCQHNnEbeC53xSDMbb70dz8IIljA=="], - "@pmndrs/uikit-lucide": ["@pmndrs/uikit-lucide@1.0.64", "", { "dependencies": { "@pmndrs/uikit": "^1.0.64" } }, "sha512-WU9CqboaBJHLvAk3Qt6V+7oMSk2lafY/ZwdcjuwSt3U+ZExHMnM24APeBdwi6w0e0sER2I/qiSVDs26JPOg7ew=="], + "@pmndrs/uikit-lucide": ["@pmndrs/uikit-lucide@1.0.67", "", { "dependencies": { "@pmndrs/uikit": "^1.0.67" } }, "sha512-/qJlNX/xd+KsbS2SdU/7VBGGk67VS4tx2X/2A6FeMa2L2/NsvLjs7mReoRLR2DvKVHsz6GxPJd0m3uSoRjuELw=="], - "@pmndrs/uikit-pub-sub": ["@pmndrs/uikit-pub-sub@1.0.64", "", { "dependencies": { "@preact/signals-core": "^1.8.0" } }, "sha512-+hlc5aWC3wQlA/zihwh+OGSm3aY9O+L/TEuNLwXw6R8syHMv7obqeY6s/hcjzD92k6DRQZLhVzVy+yqqVKAbRA=="], + "@pmndrs/uikit-pub-sub": ["@pmndrs/uikit-pub-sub@1.0.67", "", { "dependencies": { "@preact/signals-core": "^1.8.0" } }, "sha512-WC14YjujQ+gIliaEN06/7zygIwxKzweasNicwqVjTQ1PRJLe/HK9pyyS7DBAtfUy9y/9i5dfVFJLhGI8R61dWA=="], "@preact/signals": ["@preact/signals@1.3.4", "", { "dependencies": { "@preact/signals-core": "^1.7.0" }, "peerDependencies": { "preact": "10.x" } }, "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g=="], - "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], + "@preact/signals-core": ["@preact/signals-core@1.14.1", "", {}, "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -580,15 +547,15 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@react-grab/cli": ["@react-grab/cli@0.1.29", "", { "dependencies": { "@antfu/ni": "^0.23.0", "commander": "^14.0.0", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^8.2.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.0" }, "bin": { "react-grab": "dist/cli.js" } }, "sha512-idgTec/ganPsag5BvbR3srF42UNNoUwHY+p94YORk5sCBl9ZaGJOXcDO93F8M4wruzTqIZgf1MUeY0+sTo0fVA=="], + "@react-grab/cli": ["@react-grab/cli@0.1.33", "", { "dependencies": { "@antfu/ni": "^30.1.0", "commander": "^14.0.3", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^9.4.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.1" }, "bin": { "react-grab": "bin/cli.js" } }, "sha512-UOc3PwN11Osw0NzaxRLK8trP4X+5iW1Dst3gvHRCafe3wXHyadzHYH8H1hdkcXdlIx3gsoD9ASJ+G/JH+A/jqA=="], "@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="], - "@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="], + "@react-three/fiber": ["@react-three/fiber@9.6.1", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg=="], - "@react-three/uikit": ["@react-three/uikit@1.0.64", "", { "dependencies": { "@pmndrs/pointer-events": "^6.6.29", "@pmndrs/uikit": "^1.0.64", "@preact/signals-core": "^1.5.1", "suspend-react": "^0.1.3", "zustand": "^5.0.6" }, "peerDependencies": { "@react-three/fiber": ">=8", "react": ">=18" } }, "sha512-//wUWePJHHHWgDokWs2WUs8yUqOJ1Dy/g1OufGHw/BeTujtoNG0hmuISjdeDXZOQPfNjpMgrbemfmvryTeCfLg=="], + "@react-three/uikit": ["@react-three/uikit@1.0.67", "", { "dependencies": { "@pmndrs/pointer-events": "^6.6.29", "@pmndrs/uikit": "^1.0.67", "@preact/signals-core": "^1.5.1", "suspend-react": "^0.1.3", "zustand": "^5.0.6" }, "peerDependencies": { "@react-three/fiber": ">=8", "react": ">=18" } }, "sha512-yq0dL8S0NDY2ffudRtnEGvrntRAJPwMGKWnJFZdbulP1AXgTwpiSV4ES6zlwjiXL4or3FiOeV0Vz3T94SGUSMw=="], - "@react-three/uikit-lucide": ["@react-three/uikit-lucide@1.0.64", "", { "dependencies": { "@pmndrs/uikit-lucide": "^1.0.64", "@react-three/uikit": "^1.0.64" } }, "sha512-7csHVPDa6OoKog3HrRo4E9OuC4n98Fj9iDsZnNbNUt0YjocoUCBygCStSqaFcZnppgy78eMk1qS6nfu78R0AZw=="], + "@react-three/uikit-lucide": ["@react-three/uikit-lucide@1.0.67", "", { "dependencies": { "@pmndrs/uikit-lucide": "^1.0.67", "@react-three/uikit": "^1.0.67" } }, "sha512-MsJYwLFibaX3aa8FFiJPz1Vm9tIAXyrWxIdkiaigKPlaM3yIo7DkTY1E5Xkq10X+EP7f48UjNDa1cbX0eqXo6g=="], "@repo/eslint-config": ["@repo/eslint-config@workspace:packages/eslint-config"], @@ -600,65 +567,67 @@ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-eu3eFRmE9NjgZ0wPdRJ44l+LGSeIky+tz5ZQd8zQkw/Yqi+BM7wq+8nbabeoiVUcICi/IZweMOKl/MCmkrd1+g=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RUkAE404z/J8NsyrUosMcBaXT6M4bRFxTQrmkDQBLQVXaC8Jl0e9bMvYDSX0GW7Ffm2m3j9y7RXgR1foeUAM9w=="], - "@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.12", "", { "os": "linux", "cpu": "x64" }, "sha512-InIUtH7cw/vqXNX1Gr7QgWfmw3ct08pV5CpfdEOR48z2u2rzdmpIuk00B/Q2xCb0PMWtKgiMQynfuphmEuUyTQ=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-lC6nD//Xh67fmJM0LKaLsg74Wry0aYrgMklpiNgCbUaMdPIOqj0A00iri3NU7Lb7pZHx8ViisgpeDKlpSgFUCA=="], - "@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.12", "", { "os": "win32", "cpu": "x64" }, "sha512-conYri8VUl72JOdYnLDPYwzqbPcY5ECoHmo9FWoKznemhaAIilj4maHqs9Uar0aKfNoZIULniy+6iWaLtLO34A=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-XoR4bsg62/L/esRVcmoMESEiNZ36+YmyjYGLpoqk8nwMgXzzVjNOgX0lRSz5w/U/ajLGv3nhMsS0Q2QOdvp2AQ=="], "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/draco3d": ["@types/draco3d@1.4.10", "", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/howler": ["@types/howler@2.2.12", "", {}, "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + "@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], @@ -668,25 +637,25 @@ "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="], @@ -696,8 +665,6 @@ "@visual-json/react": ["@visual-json/react@0.4.0", "", { "dependencies": { "@visual-json/core": "0.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-qiDrXI7wu9VoTE0TaLoJp7kwRj8SQTpkS/wkts5sBPA8078zdrIKc+0u0Y8wAXGYd3udw9dNfD4kmpO48OEwXQ=="], - "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], - "@zappar/msdf-generator": ["@zappar/msdf-generator@1.2.4", "", { "dependencies": { "comlink": "^4.4.2" } }, "sha512-6S/MCk0Ky0ipewZJw4xFEzH/2aYfWmPXEkTdBtNyDDfkbicrNwgJgtxZ4SnTDyNe9XHMqDA4sL9srRsgDLRMqA=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -708,7 +675,7 @@ "agentation": ["agentation@2.3.3", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-AUZgFCdBQ/nAohlFsHByM9S2Dp7ECMNqVjlOke4hv/90v+wTiwrGladEkgWS60RDQp+CJ5p97meeCthYgTFlKQ=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -742,25 +709,27 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "bippy": ["bippy@0.5.32", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-yt1mC8eReTxjfg41YBZdN4PvsDwHFWxltoiQX0Q+Htlbf41aSniopb7ECZits01HwNAvXEh69RGk/ImlswDTEw=="], + "bippy": ["bippy@0.5.39", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-8hE8rKSl8JWyeaY+JjpnmceWAZPpLEyzOZQpWXM5Rc7861c5WotMJHy2aRZKZrGA8nMpvLNF01t4yQQ+HcZG3w=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -770,17 +739,17 @@ "camera-controls": ["camera-controls@3.1.2", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -844,7 +813,7 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dotenv-cli": ["dotenv-cli@11.0.0", "", { "dependencies": { "cross-spawn": "^7.0.6", "dotenv": "^17.1.0", "dotenv-expand": "^12.0.0", "minimist": "^1.2.6" }, "bin": { "dotenv": "cli.js" } }, "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww=="], @@ -858,23 +827,19 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="], - - "element-source": ["element-source@0.0.3", "", { "dependencies": { "bippy": "^0.5.32" } }, "sha512-o3VMv2BIfY/axhIBKlE9HrR5rNqnhjHN2PEAKxG65O0VCSfONoMi9QMQjY12XVVvMuTzr1cAg/4xLMkvh+/Wlg=="], - - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], + "enhanced-resolve": ["enhanced-resolve@5.21.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ=="], - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-iterator-helpers": ["es-iterator-helpers@1.3.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" } }, "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ=="], + "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], @@ -884,8 +849,6 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -902,7 +865,7 @@ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], - "eslint-plugin-turbo": ["eslint-plugin-turbo@2.8.20", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-sMDremg73XbX+Imh7iDRS3TAro5jIf7CdzSqrT50wCGhClbiUvmWcLomnX+ldUbfTy+OvtqOeePvTivJMR87eQ=="], + "eslint-plugin-turbo": ["eslint-plugin-turbo@2.9.12", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-Ilv1DTYyghdIdTUsW/VbjVTYKt1Hfs7X2C+rq/i4u7ZVofzcHZwk/3QwrV5UyOGADmxmWNyD+xcRS7+ZE5arQg=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -918,7 +881,7 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -926,11 +889,11 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.7", "", {}, "sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA=="], + "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "express-rate-limit": ["express-rate-limit@8.5.1", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -940,7 +903,13 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -974,13 +943,15 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], + "geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="], "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -1016,11 +987,11 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - "hls.js": ["hls.js@1.6.15", "", {}, "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA=="], + "hls.js": ["hls.js@1.6.16", "", {}, "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA=="], - "hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="], + "hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], "howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="], @@ -1044,7 +1015,7 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1058,7 +1029,7 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], @@ -1112,9 +1083,9 @@ "its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1172,13 +1143,13 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], - "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + "lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="], "maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="], @@ -1220,7 +1191,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], + "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -1230,11 +1201,11 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "number-flow": ["number-flow@0.5.12", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-CIs21h2JkfYG4rfgERaUNAk0Cz+Ef14fNJfSCbGGhgRgconQc9b7rcCQfi9SZ36kNjVXmsl2BrzDbjGtEgumAA=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1258,7 +1229,7 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -1266,6 +1237,8 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -1290,11 +1263,11 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="], - "preact": ["preact@10.29.0", "", {}, "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg=="], + "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -1316,11 +1289,11 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], - "react-grab": ["react-grab@0.1.29", "", { "dependencies": { "@medv/finder": "^4.0.2", "@react-grab/cli": "0.1.29", "bippy": "^0.5.32", "element-source": "^0.0.3", "solid-js": "^1.9.10" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-s6CDIwjA0pzhCJqgFabM3GZ6m9vsmStOHopl7c8Xyx702QZ5ZXxOMRTpd2/7Gg+GC/1XwFmyLHwQiOCbUYIIYQ=="], + "react-grab": ["react-grab@0.1.33", "", { "dependencies": { "@react-grab/cli": "0.1.33", "bippy": "^0.5.39" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-ER919JMsE4TTrb2CpEivqsIjNMSycD4HtS8v7mS3pq67U7WL1K3+C8m9AYOwW4dpuYh+EanC2eJBmfuczHJZ0A=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -1328,7 +1301,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-scan": ["react-scan@0.5.3", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/generator": "^7.26.2", "@babel/types": "^7.26.0", "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "@types/node": "^20.17.9", "bippy": "^0.5.30", "commander": "^14.0.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "picocolors": "^1.1.1", "preact": "^10.25.1", "prompts": "^2.4.2" }, "optionalDependencies": { "unplugin": "2.1.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "bin": { "react-scan": "bin/cli.js" } }, "sha512-qde9PupmUf0L3MU1H6bjmoukZNbCXdMyTEwP4Gh8RQ4rZPd2GGNBgEKWszwLm96E8k+sGtMpc0B9P0KyFDP6Bw=="], + "react-scan": ["react-scan@0.5.6", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/types": "^7.26.0", "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "bippy": "^0.5.39", "commander": "^14.0.0", "picocolors": "^1.1.1", "preact": "^10.25.1", "prompts": "^2.4.2", "react-grab": "latest" }, "optionalDependencies": { "unplugin": "2.1.0" }, "peerDependencies": { "esbuild": ">=0.18.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["esbuild"], "bin": { "react-scan": "bin/cli.js" } }, "sha512-FrZ75yWjabNQmBbN9wxP+FgfgcPYoOFFWXYBhi6OnUbF/DyJDwOEfV2RRs9IWVfPVjhsku2CPK3oohYC5GEHpg=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -1352,7 +1325,7 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], @@ -1366,10 +1339,6 @@ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], - - "seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="], - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -1388,7 +1357,7 @@ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], @@ -1400,8 +1369,6 @@ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], - "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], @@ -1410,11 +1377,11 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -1440,9 +1407,9 @@ "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], - "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="], @@ -1452,9 +1419,9 @@ "three-stdlib": ["three-stdlib@2.36.1", "", { "dependencies": { "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", "@types/webxr": "^0.5.2", "draco3d": "^1.4.1", "fflate": "^0.6.9", "potpack": "^1.0.1" }, "peerDependencies": { "three": ">=0.128.0" } }, "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg=="], - "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1472,7 +1439,7 @@ "tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="], - "turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="], + "turbo": ["turbo@2.9.12", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.12", "@turbo/darwin-arm64": "2.9.12", "@turbo/linux-64": "2.9.12", "@turbo/linux-arm64": "2.9.12", "@turbo/windows-64": "2.9.12", "@turbo/windows-arm64": "2.9.12" }, "bin": { "turbo": "bin/turbo" } }, "sha512-lCPgus1NuTiBdaITWqzSH/Ff6HVL8HHGBtOXHg1dHRfcshN79XkygSdh0M6g8b0td91ILLG5MTkLOkp5UvyPJw=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], @@ -1488,15 +1455,15 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - "typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="], + "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], - "ultracite": ["ultracite@7.3.2", "", { "dependencies": { "@clack/prompts": "^1.1.0", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-JV2TdeBDc1aaj3wFzXLjRcBlUZkXBdShMLAjHh9YLK0rXPSK42CzHqfWXUfibFUAUiUGNwoOFnIpS45EaJAcdg=="], + "ultracite": ["ultracite@7.6.5", "", { "dependencies": { "@clack/prompts": "^1.3.0", "commander": "^14.0.3", "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.6", "yaml": "^2.8.4", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.0.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-q68Ht5cTvjzmyDvhwZUPfxWLpcwioG5e5ODrVxBAbc0qWEn5hD3+CT1Tgq8FEFEypjKVote01inkG1nDOE9hmw=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -1538,31 +1505,33 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="], - "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], + "zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@eslint/eslintrc/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@pascal-app/editor/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@pascal-app/mcp/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "@pascal-app/mcp/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], - - "@pascal-app/viewer/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@pascal-app/viewer/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1586,47 +1555,43 @@ "@react-three/drei/three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="], - "@repo/ui/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@repo/eslint-config/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@repo/ui/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], + "@repo/ui/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "editor/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "editor/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="], - "eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "eslint-plugin-turbo/dotenv": ["dotenv@16.0.3", "", {}, "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - - "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -1634,15 +1599,11 @@ "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "react-scan/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + "postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "stats-gl/@types/three": ["@types/three@0.183.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~1.0.1" } }, "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw=="], + "sharp/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], @@ -1652,19 +1613,19 @@ "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@pascal-app/mcp/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "@pascal-app/viewer/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@pascal-app/viewer/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "@repo/ui/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "stats-gl/@types/three/meshoptimizer": ["meshoptimizer@1.0.1", "", {}, "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g=="], + "next/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/package.json b/package.json index 10b3d7f5..bb9acd33 100644 --- a/package.json +++ b/package.json @@ -12,23 +12,40 @@ "check:fix": "biome check --write", "check-types": "turbo run check-types", "kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'", - "release": "gh workflow run release.yml -f package=both -f bump=patch", + "release": "gh workflow run release.yml -f package=all -f bump=patch", "release:viewer": "gh workflow run release.yml -f package=viewer -f bump=patch", "release:core": "gh workflow run release.yml -f package=core -f bump=patch", - "release:minor": "gh workflow run release.yml -f package=both -f bump=minor", - "release:major": "gh workflow run release.yml -f package=both -f bump=major" + "release:editor": "gh workflow run release.yml -f package=editor -f bump=patch", + "release:mcp": "gh workflow run release.yml -f package=mcp -f bump=patch", + "release:minor": "gh workflow run release.yml -f package=all -f bump=minor", + "release:major": "gh workflow run release.yml -f package=all -f bump=major" }, "devDependencies": { "@biomejs/biome": "^2.4.6", "dotenv-cli": "^11.0.0", "turbo": "^2.8.15", - "typescript": "5.9.3", + "typescript": "6.0.2", "ultracite": "^7.2.5" }, "engines": { "node": ">=18" }, "packageManager": "bun@1.3.0", + "overrides": { + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@types/three": "0.184.0" + }, + "optionalDependencies": { + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0" + }, "workspaces": [ "apps/*", "packages/*", diff --git a/packages/core/package.json b/packages/core/package.json index 8cffdb40..0b2322b3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@pascal-app/core", - "version": "0.6.0", + "version": "0.8.0", "description": "Core library for Pascal 3D building editor", "type": "module", "main": "./dist/index.js", @@ -73,9 +73,10 @@ }, "devDependencies": { "@pascal/typescript-config": "*", + "@types/bun": "^1.3.0", "@types/react": "^19.2.2", - "typescript": "5.9.3", - "@types/three": "^0.184.0" + "@types/three": "^0.184.0", + "typescript": "6.0.2" }, "keywords": [ "3d", diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts index d539ada6..02fcf1e6 100644 --- a/packages/core/src/events/bus.ts +++ b/packages/core/src/events/bus.ts @@ -29,12 +29,13 @@ export interface GridEvent { /** World-space intersection point on the grid plane. */ position: [number, number, number] /** - * Building-local intersection point, relative to the currently selected building. + * Building-local intersection point — relative to the currently selected building. * Equals `position` when no building is selected. - * Use this for placing or committing anything that lives inside a building - * (walls, slabs, items, etc.). + * Use this for placing/committing anything that lives inside a building (walls, slabs, items, etc.). */ localPosition: [number, number, number] + faceIndex?: number + object: Object3D nativeEvent: ThreeEvent } @@ -68,7 +69,7 @@ export type WindowEvent = NodeEvent export type DoorEvent = NodeEvent export type ElevatorEvent = NodeEvent -// Event suffixes, exported for use in hooks +// Event suffixes - exported for use in hooks export const eventSuffixes = [ 'click', 'move', @@ -101,7 +102,7 @@ export interface ThumbnailGenerateEvent { /** * When true, snap levels to their true positions before capturing (for a * consistent auto-thumbnail angle) and defer the capture if the tab is - * hidden, the background auto-save path. Omit for user-driven captures + * hidden — the background auto-save path. Omit for user-driven captures * that should fire immediately from the current camera pose. */ snapLevels?: boolean @@ -109,8 +110,10 @@ export interface ThumbnailGenerateEvent { export interface CameraControlFitSceneEvent { /** - * XZ-plane axis-aligned bounds for camera framing. Omitted values let the - * listener choose its default framing pose. + * XZ-plane axis-aligned bounds of the scene's geometry, computed from the + * scene graph (see `@pascal-app/editor`'s `computeSceneBoundsXZ`). The + * viewer's camera-controls listener frames the camera onto this box. + * Omitted values fall back to the camera's default pose. */ bounds?: { min: [number, number] diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fcc7dd29..aa94614d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -46,6 +46,8 @@ export { getRenderableSlabPolygon } from './lib/slab-polygon' export { detectSpacesForLevel, initSpaceDetectionSync, + planAutoSlabsForLevel, + type AutoSlabSyncPlan, type Space, wallTouchesOthers, } from './lib/space-detection' @@ -122,6 +124,15 @@ export { type WallMiterBoundaryPoints, type WallMiterData, } from './systems/wall/wall-mitering' +export { + constrainWallMoveDeltaToAxis, + getPerpendicularWallMoveAxis, + planWallMoveJunctions, + type WallMoveBridgePlan, + type WallMoveAxis, + type WallMoveJunctionPlan, + type WallPlanPoint, +} from './systems/wall/wall-move' export type { SceneGraph } from './utils/clone-scene-graph' export { cloneLevelSubtree, cloneSceneGraph, forkSceneGraph } from './utils/clone-scene-graph' export { isObject } from './utils/types' diff --git a/packages/core/src/lib/polygon-geometry.ts b/packages/core/src/lib/polygon-geometry.ts index f8f639b5..b2689b84 100644 --- a/packages/core/src/lib/polygon-geometry.ts +++ b/packages/core/src/lib/polygon-geometry.ts @@ -6,10 +6,7 @@ export function insetPolygonFromCentroid( return polygon.map(([x, z]) => [x, z] as [number, number]) } - const centroid = polygon.reduce( - (acc, [x, z]) => ({ x: acc.x + x, z: acc.z + z }), - { x: 0, z: 0 }, - ) + const centroid = polygon.reduce((acc, [x, z]) => ({ x: acc.x + x, z: acc.z + z }), { x: 0, z: 0 }) centroid.x /= Math.max(polygon.length, 1) centroid.z /= Math.max(polygon.length, 1) @@ -70,7 +67,10 @@ function dedupePolygonPoints( return deduped } -function simplifyPolyline(points: Array<[number, number]>, tolerance: number): Array<[number, number]> { +function simplifyPolyline( + points: Array<[number, number]>, + tolerance: number, +): Array<[number, number]> { if (points.length <= 2) { return points.map(([x, z]) => [x, z] as [number, number]) } diff --git a/packages/core/src/lib/space-detection.ts b/packages/core/src/lib/space-detection.ts index eb7ef6cc..94a6e47b 100644 --- a/packages/core/src/lib/space-detection.ts +++ b/packages/core/src/lib/space-detection.ts @@ -1,14 +1,20 @@ import { - getClampedWallCurveOffset, - getWallCurveFrameAt, - isCurvedWall, -} from '../systems/wall/wall-curve' -import { CeilingNode, SlabNode, type CeilingNode as CeilingNodeType, type SlabNode as SlabNodeType, type WallNode } from '../schema' + CeilingNode, + type CeilingNode as CeilingNodeType, + SlabNode, + type SlabNode as SlabNodeType, + type WallNode, +} from '../schema' import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control' +import { + getClampedWallCurveOffset, + getWallCurveFrameAt, + isCurvedWall, +} from '../systems/wall/wall-curve' import { simplifyClosedPolygon } from './polygon-geometry' type Point2D = { x: number; y: number } @@ -35,6 +41,12 @@ type DetectedRoom = { bbox: ReturnType } +export type AutoSlabSyncPlan = { + create: SlabNodeType[] + update: Array<{ id: SlabNodeType['id']; data: Partial }> + delete: Array +} + const DEFAULT_AUTO_SLAB_ELEVATION = 0.05 const DEFAULT_AUTO_CEILING_HEIGHT = 2.5 const ROOM_CURVE_TOLERANCE = 0.04 @@ -147,10 +159,10 @@ function polygonCentroid(points: Point2D[]) { } function bboxOf(points: Point2D[]) { - let minX = Infinity - let minY = Infinity - let maxX = -Infinity - let maxY = -Infinity + let minX = Number.POSITIVE_INFINITY + let minY = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let maxY = Number.NEGATIVE_INFINITY for (const point of points) { minX = Math.min(minX, point.x) @@ -216,7 +228,13 @@ function sampleWallPointsForRoomDetection( return [start, end] } - const subdivide = (t0: number, p0: Point2D, t1: number, p1: Point2D, depth: number): Point2D[] => { + const subdivide = ( + t0: number, + p0: Point2D, + t1: number, + p1: Point2D, + depth: number, + ): Point2D[] => { const midT = (t0 + t1) / 2 const midPoint = getWallCurveFrameAt(wall, midT).point const deviation = pointLineDistance(midPoint, p0, p1) @@ -361,7 +379,7 @@ function extractRoomPolygons(walls: WallNode[]): Point2D[][] { const signedArea = polygonArea(polygon) if (signedArea <= 0) continue - if (signedArea < 0.5 || signedArea > 10000) continue + if (signedArea < 0.5 || signedArea > 10_000) continue const signature = polygonSignature(polygon) if (faces.some((face) => polygonSignature(face) === signature)) continue @@ -442,10 +460,7 @@ function nextAutoRoomName( return `Room ${maxIndex + 1} ${suffix}` } -function sameTuplePolygon( - current: Array<[number, number]>, - next: Array<[number, number]>, -) { +function sameTuplePolygon(current: Array<[number, number]>, next: Array<[number, number]>) { return ( current.length === next.length && current.every((point, index) => point[0] === next[index]?.[0] && point[1] === next[index]?.[1]) @@ -479,12 +494,10 @@ function buildSpace(levelId: string, polygon: Point2D[]): Space { } } -function syncAutoSlabsForLevel( - levelId: string, +export function planAutoSlabsForLevel( roomPolygons: Point2D[][], existingSlabs: SlabNodeType[], - sceneStore: any, -) { +): AutoSlabSyncPlan { const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls) const manualSignatures = new Set( manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))), @@ -609,16 +622,31 @@ function syncAutoSlabsForLevel( ) } - if (slabsToDelete.length > 0) { - sceneStore.getState().deleteNodes(slabsToDelete) + return { + create: slabsToCreate, + update: slabsToUpdate, + delete: slabsToDelete, + } +} + +function syncAutoSlabsForLevel( + levelId: string, + roomPolygons: Point2D[][], + existingSlabs: SlabNodeType[], + sceneStore: any, +) { + const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs) + + if (plan.delete.length > 0) { + sceneStore.getState().deleteNodes(plan.delete) } - if (slabsToUpdate.length > 0) { - sceneStore.getState().updateNodes(slabsToUpdate) + if (plan.update.length > 0) { + sceneStore.getState().updateNodes(plan.update) } - if (slabsToCreate.length > 0) { - sceneStore.getState().createNodes(slabsToCreate.map((node) => ({ node, parentId: levelId }))) + if (plan.create.length > 0) { + sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId }))) } } @@ -727,7 +755,9 @@ function syncAutoCeilingsForLevel( const polygon = updatesById.get(ceiling.id) if (!polygon) return [] - return sameTuplePolygon(ceiling.polygon, polygon) ? [] : [{ id: ceiling.id, data: { polygon } }] + return sameTuplePolygon(ceiling.polygon, polygon) + ? [] + : [{ id: ceiling.id, data: { polygon } }] }) const plannedCeilingsForNaming: Array<{ name?: string }> = [...existingCeilings] diff --git a/packages/core/src/material-library.ts b/packages/core/src/material-library.ts index 5b08bec1..a3622cbe 100644 --- a/packages/core/src/material-library.ts +++ b/packages/core/src/material-library.ts @@ -1,5 +1,7 @@ import { type MaterialPresetPayload, + type MaterialTarget, + MaterialTarget as MaterialTargetSchema, } from './schema/material' export type MaterialCatalogItem = { @@ -12,6 +14,32 @@ export type MaterialCatalogItem = { preset: MaterialPresetPayload } +const WALL_TARGETS: MaterialTarget[] = [MaterialTargetSchema.enum.wall] + +const SLAB_TARGETS: MaterialTarget[] = [MaterialTargetSchema.enum.slab] + +const WALL_AND_SLAB_TARGETS: MaterialTarget[] = [ + MaterialTargetSchema.enum.wall, + MaterialTargetSchema.enum.slab, +] + +const STAIR_TARGETS: MaterialTarget[] = [ + MaterialTargetSchema.enum.stair, + MaterialTargetSchema.enum['stair-segment'], +] + +const STAIR_AND_FENCE_TARGETS: MaterialTarget[] = [ + ...STAIR_TARGETS, + MaterialTargetSchema.enum.fence, +] + +const ROOF_TARGETS: MaterialTarget[] = [ + MaterialTargetSchema.enum.roof, + MaterialTargetSchema.enum['roof-segment'], +] + +const CEILING_TARGETS: MaterialTarget[] = [MaterialTargetSchema.enum.ceiling] + export const MATERIAL_CATEGORIES = [ 'wood', 'wallpaper', diff --git a/packages/core/src/schema/asset-url.test.ts b/packages/core/src/schema/asset-url.test.ts index 85facfc7..61ead092 100644 --- a/packages/core/src/schema/asset-url.test.ts +++ b/packages/core/src/schema/asset-url.test.ts @@ -1,7 +1,3 @@ -// @ts-expect-error — bun:test is provided by the Bun runtime; core does not -// depend on @types/bun so the import type is unresolved at compile time. -// The tsconfig in packages/core still emits this file; the @ts-expect-error -// keeps the build green while letting `bun test` pick it up normally. import { afterEach, beforeEach, describe, expect, test } from 'bun:test' import { ALLOWED_ORIGINS_ENV, AssetUrl } from './asset-url' diff --git a/packages/core/src/schema/nodes/fence.ts b/packages/core/src/schema/nodes/fence.ts index fe84c885..40875717 100644 --- a/packages/core/src/schema/nodes/fence.ts +++ b/packages/core/src/schema/nodes/fence.ts @@ -13,9 +13,9 @@ export const FenceNode = BaseNode.extend({ materialPreset: z.string().optional(), start: z.tuple([z.number(), z.number()]), end: z.tuple([z.number(), z.number()]), - curveOffset: z.number().optional(), height: z.number().default(1.8), thickness: z.number().default(0.08), + curveOffset: z.number().optional(), baseHeight: z.number().default(0.22), postSpacing: z.number().default(2), postSize: z.number().default(0.1), @@ -30,8 +30,8 @@ export const FenceNode = BaseNode.extend({ dedent` Fence node - used to represent a fence segment in the building/site level coordinate system - start/end: fence endpoints in level coordinate system - - curveOffset: midpoint sagitta offset used to bend the fence into an arc - height/thickness: overall fence dimensions in meters + - curveOffset: midpoint sagitta offset used to bend the fence into an arc - baseHeight/postSpacing/postSize/topRailHeight: exact geometric controls from the plan3D fence model - groundClearance/edgeInset/baseStyle: fence support and inset configuration - showInfill: whether to draw intermediate posts/slats between end posts diff --git a/packages/core/src/schema/nodes/item.ts b/packages/core/src/schema/nodes/item.ts index 5fb0f15e..f8434d35 100644 --- a/packages/core/src/schema/nodes/item.ts +++ b/packages/core/src/schema/nodes/item.ts @@ -83,6 +83,18 @@ const assetSchema = z.object({ // Optional top-down 2D image shown inside the item's footprint on the // floor plan. When present, replaces the default diagonal-cross marker. floorPlanUrl: z.string().optional(), + // Where the item came from in the catalog. Used by the editor's items + // panel to filter Library / Community / Mine. The server populates it + // from `items.userId`: null → 'library', current user → 'mine', + // other user → 'community'. Defaults to 'library' when absent (e.g. + // the seeded built-in catalog). + source: z.enum(['library', 'community', 'mine']).default('library'), + // True when the item belongs to the caller and is still in draft status. + // The catalog only loads my drafts (other users' drafts are never + // published to the catalog). Used so the Community filter can include + // *my* published items alongside other users', while leaving drafts + // visible only under Mine. + isDraft: z.boolean().optional(), src: AssetUrl, dimensions: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]), // [w, h, d] attachTo: z.enum(['wall', 'wall-side', 'ceiling']).optional(), diff --git a/packages/core/src/schema/nodes/roof.ts b/packages/core/src/schema/nodes/roof.ts index 944cca04..0dd2f39a 100644 --- a/packages/core/src/schema/nodes/roof.ts +++ b/packages/core/src/schema/nodes/roof.ts @@ -1,25 +1,26 @@ import dedent from 'dedent' import { z } from 'zod' import { BaseNode, nodeType, objectId } from '../base' -import { type MaterialSchema, MaterialSchema as MaterialSchemaSchema } from '../material' +import type { MaterialSchema as MaterialSchemaType } from '../material' +import { MaterialSchema } from '../material' import { RoofSegmentNode } from './roof-segment' export type RoofSurfaceMaterialRole = 'top' | 'edge' | 'wall' export type RoofSurfaceMaterialSpec = { - material?: MaterialSchema + material?: MaterialSchemaType materialPreset?: string } export const RoofNode = BaseNode.extend({ id: objectId('roof'), type: nodeType('roof'), - material: MaterialSchemaSchema.optional(), + material: MaterialSchema.optional(), materialPreset: z.string().optional(), - topMaterial: MaterialSchemaSchema.optional(), + topMaterial: MaterialSchema.optional(), topMaterialPreset: z.string().optional(), - edgeMaterial: MaterialSchemaSchema.optional(), + edgeMaterial: MaterialSchema.optional(), edgeMaterialPreset: z.string().optional(), - wallMaterial: MaterialSchemaSchema.optional(), + wallMaterial: MaterialSchema.optional(), wallMaterialPreset: z.string().optional(), position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]), // Rotation around Y axis in radians @@ -54,7 +55,8 @@ export function getEffectiveRoofSurfaceMaterial( if (node.topMaterial !== undefined || typeof node.topMaterialPreset === 'string') { return { material: node.topMaterial, - materialPreset: typeof node.topMaterialPreset === 'string' ? node.topMaterialPreset : undefined, + materialPreset: + typeof node.topMaterialPreset === 'string' ? node.topMaterialPreset : undefined, } } } diff --git a/packages/core/src/schema/nodes/stair.ts b/packages/core/src/schema/nodes/stair.ts index 81969828..a0c68fe1 100644 --- a/packages/core/src/schema/nodes/stair.ts +++ b/packages/core/src/schema/nodes/stair.ts @@ -1,7 +1,8 @@ import dedent from 'dedent' import { z } from 'zod' import { BaseNode, nodeType, objectId } from '../base' -import { type MaterialSchema, MaterialSchema as MaterialSchemaSchema } from '../material' +import type { MaterialSchema as MaterialSchemaType } from '../material' +import { MaterialSchema } from '../material' import { StairSegmentNode } from './stair-segment' export const StairRailingMode = z.enum(['none', 'left', 'right', 'both']) @@ -15,20 +16,20 @@ export type StairTopLandingMode = z.infer export type StairSlabOpeningMode = z.infer export type StairSurfaceMaterialRole = 'railing' | 'tread' | 'side' export type StairSurfaceMaterialSpec = { - material?: MaterialSchema + material?: MaterialSchemaType materialPreset?: string } export const StairNode = BaseNode.extend({ id: objectId('stair'), type: nodeType('stair'), - material: MaterialSchemaSchema.optional(), + material: MaterialSchema.optional(), materialPreset: z.string().optional(), - railingMaterial: MaterialSchemaSchema.optional(), + railingMaterial: MaterialSchema.optional(), railingMaterialPreset: z.string().optional(), - treadMaterial: MaterialSchemaSchema.optional(), + treadMaterial: MaterialSchema.optional(), treadMaterialPreset: z.string().optional(), - sideMaterial: MaterialSchemaSchema.optional(), + sideMaterial: MaterialSchema.optional(), sideMaterialPreset: z.string().optional(), position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]), // Rotation around Y axis in radians @@ -126,18 +127,26 @@ export function getEffectiveStairSurfaceMaterial( const treadFallback = { material: node.treadMaterial, - materialPreset: typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined, + materialPreset: + typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined, } const sideFallback = { material: node.sideMaterial, - materialPreset: typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined, + materialPreset: + typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined, } - if (role === 'tread' && (sideFallback.material !== undefined || sideFallback.materialPreset !== undefined)) { + if ( + role === 'tread' && + (sideFallback.material !== undefined || sideFallback.materialPreset !== undefined) + ) { return sideFallback } - if (role === 'side' && (treadFallback.material !== undefined || treadFallback.materialPreset !== undefined)) { + if ( + role === 'side' && + (treadFallback.material !== undefined || treadFallback.materialPreset !== undefined) + ) { return treadFallback } diff --git a/packages/core/src/store/actions/node-actions.ts b/packages/core/src/store/actions/node-actions.ts index d9e50b79..401385bb 100644 --- a/packages/core/src/store/actions/node-actions.ts +++ b/packages/core/src/store/actions/node-actions.ts @@ -9,6 +9,9 @@ import type { CollectionId } from '../../schema/collections' import type { SceneState } from '../use-scene' type AnyContainerNode = AnyNode & { children: string[] } +type NodeCreateOp = { node: AnyNode; parentId?: AnyNodeId } +type NodeUpdateOp = { id: AnyNodeId; data: Partial } +type NodeDeleteOp = AnyNodeId type WallAttachmentUpdate = { id: AnyNodeId; data: Partial } type WallMergePlan = { primaryWallId: AnyNodeId @@ -122,7 +125,7 @@ function buildMergedWallAttachmentUpdates( const wallChildren = [...(primary.children ?? []), ...(secondary.children ?? [])] as AnyNodeId[] for (const childId of wallChildren) { const child = nodes[childId] - if (!child || !('position' in child) || !Array.isArray(child.position)) { + if (!(child && 'position' in child && Array.isArray(child.position))) { continue } @@ -190,10 +193,12 @@ function buildWallMergePlans( }) const [primary, secondary] = sortedCandidates if ( - !primary || - !secondary || - !areWallStylesCompatible(primary, secondary) || - !areWallsCollinearAcrossPoint(primary, secondary, junction) + !( + primary && + secondary && + areWallStylesCompatible(primary, secondary) && + areWallsCollinearAcrossPoint(primary, secondary, junction) + ) ) { continue } @@ -230,7 +235,7 @@ function buildWallMergePlans( export const createNodesAction = ( set: (fn: (state: SceneState) => Partial) => void, get: () => SceneState, - ops: { node: AnyNode; parentId?: AnyNodeId }[], + ops: NodeCreateOp[], ) => { if (get().readOnly) return set((state) => { @@ -275,6 +280,144 @@ export const createNodesAction = ( ops.forEach(({ node, parentId }) => { get().markDirty(node.id) if (parentId) get().markDirty(parentId) + else if (node.parentId) get().markDirty(node.parentId as AnyNodeId) + }) +} + +export const applyNodeChangesAction = ( + set: (fn: (state: SceneState) => Partial) => void, + get: () => SceneState, + changes: { create?: NodeCreateOp[]; update?: NodeUpdateOp[]; delete?: NodeDeleteOp[] }, +) => { + if (get().readOnly) return + + const createOps = changes.create ?? [] + const updateOps = changes.update ?? [] + const deleteOps = changes.delete ?? [] + const nodesToMarkDirty = new Set() + const parentsToMarkDirty = new Set() + + set((state) => { + const nextNodes = { ...state.nodes } + const nextCollections = { ...state.collections } + const nextRootIds = [...state.rootNodeIds] + let resolvedRootIds = nextRootIds + + for (const { id, data } of updateOps) { + const currentNode = nextNodes[id] + if (!currentNode) continue + + if (data.parentId !== undefined && data.parentId !== currentNode.parentId) { + const oldParentId = currentNode.parentId as AnyNodeId | null + if (oldParentId && nextNodes[oldParentId]) { + const oldParent = nextNodes[oldParentId] as AnyContainerNode + nextNodes[oldParent.id] = { + ...oldParent, + children: oldParent.children.filter((childId) => childId !== id), + } as AnyNode + parentsToMarkDirty.add(oldParent.id) + } + + const newParentId = data.parentId as AnyNodeId | null + if (newParentId && nextNodes[newParentId]) { + const newParent = nextNodes[newParentId] as AnyContainerNode + nextNodes[newParent.id] = { + ...newParent, + children: Array.from(new Set([...newParent.children, id])), + } as AnyNode + parentsToMarkDirty.add(newParent.id) + } + } + + nextNodes[id] = { ...currentNode, ...data } as AnyNode + nodesToMarkDirty.add(id) + } + + for (const { node, parentId } of createOps) { + const effectiveParentId = parentId ?? (node.parentId as AnyNodeId | null) ?? null + const newNode = { + ...node, + parentId: effectiveParentId, + } as AnyNode + + nextNodes[newNode.id as AnyNodeId] = newNode + nodesToMarkDirty.add(newNode.id as AnyNodeId) + + if (effectiveParentId && nextNodes[effectiveParentId]) { + const parent = nextNodes[effectiveParentId] + if ('children' in parent && Array.isArray(parent.children)) { + nextNodes[effectiveParentId] = { + ...parent, + children: Array.from(new Set([...parent.children, newNode.id])) as any, + } + parentsToMarkDirty.add(effectiveParentId) + } + } else if (!effectiveParentId && !nextRootIds.includes(newNode.id as AnyNodeId)) { + nextRootIds.push(newNode.id as AnyNodeId) + } + } + + const allIdsToDelete = new Set() + const collectDelete = (id: AnyNodeId) => { + if (allIdsToDelete.has(id)) return + allIdsToDelete.add(id) + const node = nextNodes[id] + if (node && 'children' in node && Array.isArray(node.children)) { + for (const childId of node.children) { + collectDelete(childId as AnyNodeId) + } + } + } + + for (const id of deleteOps) { + collectDelete(id) + } + + for (const id of allIdsToDelete) { + const node = nextNodes[id] + if (!node) continue + + const parentId = node.parentId as AnyNodeId | null + if (parentId && nextNodes[parentId] && !allIdsToDelete.has(parentId)) { + const parent = nextNodes[parentId] as AnyContainerNode + if (parent.children) { + nextNodes[parent.id] = { + ...parent, + children: parent.children.filter((childId) => childId !== id), + } as AnyNode + parentsToMarkDirty.add(parent.id) + } + } + + resolvedRootIds = resolvedRootIds.filter((rootId) => rootId !== id) + + if ('collectionIds' in node && node.collectionIds) { + for (const collectionId of node.collectionIds as CollectionId[]) { + const collection = nextCollections[collectionId] + if (collection) { + nextCollections[collectionId] = { + ...collection, + nodeIds: collection.nodeIds.filter((nodeId) => nodeId !== id), + } + } + } + } + + delete nextNodes[id] + } + + return { nodes: nextNodes, rootNodeIds: resolvedRootIds, collections: nextCollections } + }) + + nodesToMarkDirty.forEach((id) => get().markDirty(id)) + parentsToMarkDirty.forEach((id) => { + get().markDirty(id) + const parent = get().nodes[id] + if (parent && 'children' in parent && Array.isArray(parent.children)) { + for (const childId of parent.children) { + get().markDirty(childId as AnyNodeId) + } + } }) } diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index 51e1caa9..48615bbf 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -111,7 +111,7 @@ function migrateWallSurfaceMaterials(node: Record) { materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined, } - if (!hasInterior && !hasExterior) { + if (!(hasInterior || hasExterior)) { if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) { return node } @@ -174,7 +174,7 @@ function migrateStairSurfaceMaterials(node: Record) { return legacyFinish } - if (!hasRailing && !hasTread && !hasSide) { + if (!(hasRailing || hasTread || hasSide)) { if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) { return node } @@ -236,7 +236,7 @@ function migrateRoofSurfaceMaterials(node: Record) { materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined, } - if (!hasTop && !hasEdge && !hasWall) { + if (!(hasTop || hasEdge || hasWall)) { if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) { return node } @@ -350,7 +350,7 @@ function migrateNodes(nodes: Record): Record { } function getNodeChildIds(node: AnyNode): AnyNodeId[] { - if (!('children' in node) || !Array.isArray(node.children)) { + if (!('children' in node && Array.isArray(node.children))) { return [] } @@ -438,6 +438,11 @@ export type SceneState = { createNode: (node: AnyNode, parentId?: AnyNodeId) => void createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void + applyNodeChanges: (changes: { + create?: { node: AnyNode; parentId?: AnyNodeId }[] + update?: { id: AnyNodeId; data: Partial }[] + delete?: AnyNodeId[] + }) => void updateNode: (id: AnyNodeId, data: Partial) => void updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void @@ -511,6 +516,13 @@ const useScene: UseSceneStore = create()( } } + set({ + nodes: cleanedNodes, + rootNodeIds, + dirtyNodes: new Set(), + collections: {}, + }) + const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds) const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds) if (normalizedRootNodeIds.length > 0) { @@ -579,6 +591,7 @@ const useScene: UseSceneStore = create()( createNodes: (ops) => nodeActions.createNodesAction(set, get, ops), createNode: (node, parentId) => nodeActions.createNodesAction(set, get, [{ node, parentId }]), + applyNodeChanges: (changes) => nodeActions.applyNodeChangesAction(set, get, changes), updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates), updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]), @@ -701,8 +714,8 @@ let prevFutureLength = 0 let prevNodesSnapshot: Record | null = null export function clearSceneHistory() { - useScene.temporal.getState().clear() resetSceneHistoryPauseDepth() + useScene.temporal.getState().clear() prevPastLength = 0 prevFutureLength = 0 prevNodesSnapshot = null diff --git a/packages/core/src/systems/elevator/elevator-opening-sync.test.ts b/packages/core/src/systems/elevator/elevator-opening-sync.test.ts index bfaf09f4..ca476072 100644 --- a/packages/core/src/systems/elevator/elevator-opening-sync.test.ts +++ b/packages/core/src/systems/elevator/elevator-opening-sync.test.ts @@ -1,5 +1,3 @@ -// @ts-expect-error — bun:test is provided by the Bun runtime; core does not -// depend on @types/bun so the import type is unresolved at compile time. import { describe, expect, test } from 'bun:test' import type { AnyNode } from '../../schema' import { BuildingNode, CeilingNode, ElevatorNode, LevelNode, SlabNode } from '../../schema' diff --git a/packages/core/src/systems/stair/stair-opening-sync.test.ts b/packages/core/src/systems/stair/stair-opening-sync.test.ts index 4ab030f2..f3a194f6 100644 --- a/packages/core/src/systems/stair/stair-opening-sync.test.ts +++ b/packages/core/src/systems/stair/stair-opening-sync.test.ts @@ -1,5 +1,3 @@ -// @ts-expect-error — bun:test is provided by the Bun runtime; core does not -// depend on @types/bun so the import type is unresolved at compile time. import { describe, expect, test } from 'bun:test' import type { AnyNode } from '../../schema' import { diff --git a/packages/core/src/systems/stair/stair-opening-sync.ts b/packages/core/src/systems/stair/stair-opening-sync.ts index ca9b1904..71680e0a 100644 --- a/packages/core/src/systems/stair/stair-opening-sync.ts +++ b/packages/core/src/systems/stair/stair-opening-sync.ts @@ -35,10 +35,9 @@ type AxisAlignedRect = { maxZ: number } -const CURVED_STAIR_SLAB_OPENING_RATIO = 0.9 +const CURVED_STAIR_SLAB_OPENING_RATIO = 0.8 const STRAIGHT_STAIR_TARGET_THRESHOLD_MIN = 0.35 const STAIR_SLAB_OPENING_TIGHTENING = 0 -const CURVED_STAIR_OPENING_STEP_PADDING = 3 function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)) @@ -280,7 +279,7 @@ function polygonArea(points: Point2D[]) { for (let index = 0; index < points.length; index += 1) { const current = points[index] const next = points[(index + 1) % points.length] - if (!current || !next) continue + if (!(current && next)) continue area += current[0] * next[1] - next[0] * current[1] } return area / 2 @@ -430,39 +429,24 @@ function buildUnionPolygonsFromRects(rects: AxisAlignedRect[]): Point2D[][] { return polygons } -function getCurvedOpeningStepCount( - stair: StairNode, - innerRadius: number, - outerRadius: number, - totalSweep: number, -) { - const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10)) - const stepSweep = Math.abs(totalSweep) / stepCount - const midRadius = Math.max((innerRadius + outerRadius) * 0.5, 0.01) - const treadDepth = Math.max(stepSweep * midRadius, 0.2) - return Math.min( - stepCount, +function getCurvedOpeningPolygon(stair: StairNode): Point2D[] { + const width = Math.max(stair.width ?? 1, 0.4) + const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9) + const outerRadius = innerRadius + width + const totalSweep = stair.sweepAngle ?? Math.PI / 2 + const openingSweep = + Math.sign(totalSweep || 1) * Math.max( - 1, - Math.ceil(1.8 / treadDepth), - Math.ceil(stepCount * CURVED_STAIR_SLAB_OPENING_RATIO), - ), - ) -} - -function buildArcOpeningPolygon( - stair: StairNode, - innerRadius: number, - outerRadius: number, - startAngle: number, - endAngle: number, -): Point2D[] { - const sweep = endAngle - startAngle + Math.abs(totalSweep) * CURVED_STAIR_SLAB_OPENING_RATIO, + Math.abs(totalSweep) / Math.max(stair.stepCount ?? 1, 1), + ) + const startAngle = totalSweep / 2 - openingSweep + const endAngle = totalSweep / 2 const segmentCount = Math.max( 10, Math.min( 32, - Math.ceil(Math.abs(sweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5), + Math.ceil(Math.abs(openingSweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5), ), ) const outerPoints: Point2D[] = [] @@ -470,7 +454,7 @@ function buildArcOpeningPolygon( for (let index = 0; index <= segmentCount; index++) { const t = index / segmentCount - const angle = startAngle + sweep * t + const angle = startAngle + (endAngle - startAngle) * t outerPoints.push( toWorldPlanPoint(stair, Math.cos(angle) * outerRadius, Math.sin(angle) * outerRadius), ) @@ -478,8 +462,7 @@ function buildArcOpeningPolygon( for (let index = segmentCount; index >= 0; index--) { const t = index / segmentCount - const angle = startAngle + sweep * t - + const angle = startAngle + (endAngle - startAngle) * t innerPoints.push( toWorldPlanPoint(stair, Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius), ) @@ -488,39 +471,6 @@ function buildArcOpeningPolygon( return [...outerPoints, ...innerPoints] } -function getCurvedOpeningPolygon(stair: StairNode, targetElevation?: number): Point2D[] { - const width = Math.max(stair.width ?? 1, 0.4) - const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9) - const outerRadius = innerRadius + width - const totalSweep = stair.sweepAngle ?? Math.PI / 2 - const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10)) - const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount - const stepSweep = totalSweep / stepCount - const targetThreshold = Math.max(stepHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN) - const endAngle = totalSweep / 2 - - const fallbackStartStepIndex = Math.max( - 0, - stepCount - getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep), - ) - let startStepIndex = fallbackStartStepIndex - if (typeof targetElevation === 'number') { - for (let index = 0; index < stepCount; index += 1) { - const stepTopElevation = stepHeight * (index + 1) - if (stepTopElevation >= targetElevation - targetThreshold) { - startStepIndex = Math.max( - 0, - Math.min(fallbackStartStepIndex, index - CURVED_STAIR_OPENING_STEP_PADDING), - ) - break - } - } - } - - const startAngle = -totalSweep / 2 + stepSweep * startStepIndex - return buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle) -} - function getSpiralOpeningPolygon(stair: StairNode): Point2D[] { const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4) const segmentCount = 48 @@ -625,7 +575,7 @@ function getStairOpeningPolygons( } if (stair.stairType === 'curved') { - return [getCurvedOpeningPolygon(stair, targetElevation)] + return [getCurvedOpeningPolygon(stair)] } if (stair.stairType === 'spiral') { @@ -784,8 +734,7 @@ export function syncAutoStairOpenings(nodes: Record) { ] if ( - !polygonsEqual(existingHoles, nextHoles) || - !metadataEqual(existingMetadata, nextMetadata) + !(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata)) ) { updates.push({ id: slab.id, @@ -840,8 +789,7 @@ export function syncAutoStairOpenings(nodes: Record) { ] if ( - !polygonsEqual(existingHoles, nextHoles) || - !metadataEqual(existingMetadata, nextMetadata) + !(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata)) ) { updates.push({ id: ceiling.id, diff --git a/packages/core/src/systems/wall/wall-curve.ts b/packages/core/src/systems/wall/wall-curve.ts index 902050ba..743f14ca 100644 --- a/packages/core/src/systems/wall/wall-curve.ts +++ b/packages/core/src/systems/wall/wall-curve.ts @@ -1,5 +1,5 @@ -import type { Point2D } from './wall-mitering' import type { FenceNode, WallNode } from '../../schema' +import type { Point2D } from './wall-mitering' const CURVE_EPSILON = 1e-6 const DEFAULT_SAMPLE_SEGMENTS = 24 @@ -115,7 +115,7 @@ function getWallArcData(wall: WallCurveLike) { } const absSagitta = Math.abs(sagitta) - const radius = chord.length * chord.length / (8 * absSagitta) + absSagitta / 2 + const radius = (chord.length * chord.length) / (8 * absSagitta) + absSagitta / 2 const centerOffset = radius - absSagitta const direction = Math.sign(sagitta) || 1 const center = { @@ -183,7 +183,10 @@ export function getWallMidpointHandlePoint(wall: WallCurveLike) { export function sampleWallCenterline(wall: WallCurveLike, segments = DEFAULT_SAMPLE_SEGMENTS) { const count = Math.max(1, segments) - return Array.from({ length: count + 1 }, (_, index) => getWallCurveFrameAt(wall, index / count).point) + return Array.from( + { length: count + 1 }, + (_, index) => getWallCurveFrameAt(wall, index / count).point, + ) } export function getWallCurveLength(wall: WallCurveLike, segments = DEFAULT_SAMPLE_SEGMENTS) { diff --git a/packages/core/src/systems/wall/wall-mitering.ts b/packages/core/src/systems/wall/wall-mitering.ts index f1ab2729..e6a2f295 100644 --- a/packages/core/src/systems/wall/wall-mitering.ts +++ b/packages/core/src/systems/wall/wall-mitering.ts @@ -135,10 +135,7 @@ function findJunctions(walls: WallNode[]): Map { return actualJunctions } -function getWallDirectionFromJunction( - wall: WallNode, - endType: 'start' | 'end' | 'passthrough', -) { +function getWallDirectionFromJunction(wall: WallNode, endType: 'start' | 'end' | 'passthrough') { if (endType === 'passthrough') { return { x: wall.end[0] - wall.start[0], @@ -148,9 +145,7 @@ function getWallDirectionFromJunction( if (isCurvedWall(wall)) { const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1) - return endType === 'start' - ? frame.tangent - : { x: -frame.tangent.x, y: -frame.tangent.y } + return endType === 'start' ? frame.tangent : { x: -frame.tangent.x, y: -frame.tangent.y } } return endType === 'start' @@ -158,18 +153,12 @@ function getWallDirectionFromJunction( : { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] } } -function getWallBoundaryFrame( - wall: WallNode, - endType: 'start' | 'end', -) { +function getWallBoundaryFrame(wall: WallNode, endType: 'start' | 'end') { if (isCurvedWall(wall)) { const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1) return { point: frame.point, - tangent: - endType === 'start' - ? frame.tangent - : { x: -frame.tangent.x, y: -frame.tangent.y }, + tangent: endType === 'start' ? frame.tangent : { x: -frame.tangent.x, y: -frame.tangent.y }, normal: frame.normal, } } diff --git a/packages/core/src/systems/wall/wall-move.ts b/packages/core/src/systems/wall/wall-move.ts new file mode 100644 index 00000000..e9b6595c --- /dev/null +++ b/packages/core/src/systems/wall/wall-move.ts @@ -0,0 +1,227 @@ +import type { WallNode } from '../../schema' + +const AXIS_EPSILON = 1e-6 + +export type WallPlanPoint = [number, number] +export type WallMoveAxis = 'x' | 'z' +export type WallMoveEndpoint = 'start' | 'end' + +export type WallMoveBridgePlan> = { + wall: TWall + originalPoint: WallPlanPoint + movedEndpoint: WallMoveEndpoint +} + +export type WallMoveLinkedWallTargetPlan< + TWall extends Pick, +> = { + wall: TWall + originalPoint: WallPlanPoint + targetPoint: WallPlanPoint +} + +export type WallMoveJunctionPlan> = { + linkedWallsToMove: TWall[] + linkedWallTargetPlans: Array> + bridgePlans: Array> + wallsToDelete: TWall[] +} + +export function getPerpendicularWallMoveAxis( + start: WallPlanPoint, + end: WallPlanPoint, +): WallMoveAxis | null { + const wallDeltaX = Math.abs(end[0] - start[0]) + const wallDeltaZ = Math.abs(end[1] - start[1]) + + if (wallDeltaX < AXIS_EPSILON && wallDeltaZ < AXIS_EPSILON) return null + + return wallDeltaX >= wallDeltaZ ? 'z' : 'x' +} + +export function constrainWallMoveDeltaToAxis( + deltaX: number, + deltaZ: number, + axis: WallMoveAxis | null, +): WallPlanPoint { + if (axis === 'x') return [deltaX, 0] + if (axis === 'z') return [0, deltaZ] + return [deltaX, deltaZ] +} + +function pointsEqual(a: WallPlanPoint, b: WallPlanPoint) { + return Math.abs(a[0] - b[0]) <= AXIS_EPSILON && Math.abs(a[1] - b[1]) <= AXIS_EPSILON +} + +function wallTouchesPoint(wall: Pick, point: WallPlanPoint) { + return pointsEqual(wall.start, point) || pointsEqual(wall.end, point) +} + +function otherWallEndpoint(wall: Pick, point: WallPlanPoint) { + return pointsEqual(wall.start, point) ? wall.end : wall.start +} + +type MoveWallRelation = 'same-direction' | 'opposite-direction' | 'off-axis' | 'stationary' +type RelatedWallEntry> = { + wall: TWall + relation: MoveWallRelation +} + +function wallLengthFromPoint(wall: Pick, point: WallPlanPoint) { + const freeEndpoint = otherWallEndpoint(wall, point) + return Math.hypot(freeEndpoint[0] - point[0], freeEndpoint[1] - point[1]) +} + +function getMoveWallRelation( + wall: Pick, + sharedPoint: WallPlanPoint, + nextPoint: WallPlanPoint, +): MoveWallRelation { + const moveX = nextPoint[0] - sharedPoint[0] + const moveZ = nextPoint[1] - sharedPoint[1] + const moveLength = Math.hypot(moveX, moveZ) + + if (moveLength < AXIS_EPSILON) return 'stationary' + + const freeEndpoint = otherWallEndpoint(wall, sharedPoint) + const wallX = freeEndpoint[0] - sharedPoint[0] + const wallZ = freeEndpoint[1] - sharedPoint[1] + const wallLength = Math.hypot(wallX, wallZ) + + if (wallLength < AXIS_EPSILON) return 'stationary' + + const normalizedCross = Math.abs(moveX * wallZ - moveZ * wallX) / (moveLength * wallLength) + if (normalizedCross > 1e-4) return 'off-axis' + + const normalizedDot = (moveX * wallX + moveZ * wallZ) / (moveLength * wallLength) + return normalizedDot >= 0 ? 'same-direction' : 'opposite-direction' +} + +export function planWallMoveJunctions>( + linkedWalls: TWall[], + originalStart: WallPlanPoint, + originalEnd: WallPlanPoint, + nextStart: WallPlanPoint, + nextEnd: WallPlanPoint, +): WallMoveJunctionPlan { + const linkedWallsToMove = new Map() + const linkedWallTargetPlans = new Map>() + const bridgePlans = new Map>() + const wallsToDelete = new Map() + + const addStandardEndpointPlan = ( + endpoint: WallMoveEndpoint, + point: WallPlanPoint, + nextPoint: WallPlanPoint, + relatedWalls: Array>, + keySuffix = '', + useTargetPlans = false, + ) => { + const hasSideBranch = relatedWalls.some((entry) => entry.relation === 'off-axis') + const hasOppositeBridge = relatedWalls.some( + (entry) => entry.relation === 'opposite-direction' && hasSideBranch, + ) + + for (const { wall, relation } of relatedWalls) { + if ( + relation === 'stationary' || + relation === 'same-direction' || + (relation === 'opposite-direction' && !hasSideBranch) + ) { + if (useTargetPlans) { + linkedWallTargetPlans.set(wall.id, { + wall, + originalPoint: point, + targetPoint: nextPoint, + }) + } else { + linkedWallsToMove.set(wall.id, wall) + } + continue + } + + if (relation === 'off-axis' && hasOppositeBridge) { + continue + } + + bridgePlans.set(`${wall.id}:${endpoint}${keySuffix}`, { + wall, + originalPoint: point, + movedEndpoint: endpoint, + }) + } + } + + const addEndpointPlan = ( + endpoint: WallMoveEndpoint, + point: WallPlanPoint, + nextPoint: WallPlanPoint, + ) => { + const moveLength = Math.hypot(nextPoint[0] - point[0], nextPoint[1] - point[1]) + const linkedAtEndpoint = linkedWalls + .filter((wall) => wallTouchesPoint(wall, point)) + .map((wall) => ({ + wall, + relation: getMoveWallRelation(wall, point, nextPoint), + })) + const consumedSameDirectionWall = linkedAtEndpoint + .filter((entry) => entry.relation === 'same-direction') + .map((entry) => ({ + ...entry, + distance: wallLengthFromPoint(entry.wall, point), + })) + .filter((entry) => moveLength + AXIS_EPSILON >= entry.distance) + .sort((a, b) => a.distance - b.distance)[0] + + if (consumedSameDirectionWall) { + const pivotPoint = [...otherWallEndpoint(consumedSameDirectionWall.wall, point)] as WallPlanPoint + const bridgeSource = linkedAtEndpoint.find((entry) => entry.relation === 'opposite-direction') + + wallsToDelete.set(consumedSameDirectionWall.wall.id, consumedSameDirectionWall.wall) + linkedWallTargetPlans.set(consumedSameDirectionWall.wall.id, { + wall: consumedSameDirectionWall.wall, + originalPoint: point, + targetPoint: pivotPoint, + }) + + if (bridgeSource) { + linkedWallTargetPlans.set(bridgeSource.wall.id, { + wall: bridgeSource.wall, + originalPoint: point, + targetPoint: pivotPoint, + }) + + bridgePlans.set(`${bridgeSource.wall.id}:${endpoint}:through`, { + wall: bridgeSource.wall, + originalPoint: pivotPoint, + movedEndpoint: endpoint, + }) + return + } + + const linkedAtPivot = linkedWalls + .filter( + (wall) => wall.id !== consumedSameDirectionWall.wall.id && wallTouchesPoint(wall, pivotPoint), + ) + .map((wall) => ({ + wall, + relation: getMoveWallRelation(wall, pivotPoint, nextPoint), + })) + + addStandardEndpointPlan(endpoint, pivotPoint, nextPoint, linkedAtPivot, ':through-pivot', true) + return + } + + addStandardEndpointPlan(endpoint, point, nextPoint, linkedAtEndpoint) + } + + addEndpointPlan('start', originalStart, nextStart) + addEndpointPlan('end', originalEnd, nextEnd) + + return { + linkedWallsToMove: Array.from(linkedWallsToMove.values()), + linkedWallTargetPlans: Array.from(linkedWallTargetPlans.values()), + bridgePlans: Array.from(bridgePlans.values()), + wallsToDelete: Array.from(wallsToDelete.values()), + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 68f55834..98b1f97c 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -5,7 +5,8 @@ "rootDir": "src", "noEmit": false, "composite": true, - "incremental": true + "incremental": true, + "types": ["bun"] }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/packages/editor/package.json b/packages/editor/package.json index 24ee4e8b..1e18e364 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@pascal-app/editor", - "version": "0.6.0", + "version": "0.8.0", "description": "Pascal building editor component", "type": "module", "exports": { @@ -11,8 +11,8 @@ "check-types": "tsc --noEmit" }, "peerDependencies": { - "@pascal-app/core": "^0.6.0", - "@pascal-app/viewer": "^0.6.0", + "@pascal-app/core": "^0.8.0", + "@pascal-app/viewer": "^0.8.0", "@react-three/drei": "^10", "@react-three/fiber": "^9", "next": ">=15", @@ -44,24 +44,24 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "howler": "^2.2.4", - "lucide-react": "^0.562.0", + "lucide-react": "^1.7.0", "mitt": "^3.0.1", "motion": "^12.34.3", "nanoid": "^5.1.6", "tailwind-merge": "^3.5.0", - "three-mesh-bvh": "^0.9.8", "zod": "^4.3.6", - "zustand": "^5.0.11" + "zustand": "^5.0.11", + "three-mesh-bvh": "~0.9.8" }, "devDependencies": { - "@pascal-app/core": "^0.6.0", - "@pascal-app/viewer": "^0.6.0", + "@pascal-app/core": "^0.8.0", + "@pascal-app/viewer": "^0.8.0", "@pascal/typescript-config": "*", + "@types/bun": "^1.3.0", "@types/howler": "^2.2.12", - "@types/node": "^22.19.12", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "@types/three": "^0.184.0", - "typescript": "5.9.3" + "typescript": "6.0.2" } } diff --git a/packages/editor/src/components/editor-2d/floorplan-action-menu-layer.tsx b/packages/editor/src/components/editor-2d/floorplan-action-menu-layer.tsx index 5d2e64a5..97e64e75 100644 --- a/packages/editor/src/components/editor-2d/floorplan-action-menu-layer.tsx +++ b/packages/editor/src/components/editor-2d/floorplan-action-menu-layer.tsx @@ -16,6 +16,7 @@ export type FloorplanActionMenuEntry = { onDelete: FloorplanActionMenuHandler onMove: FloorplanActionMenuHandler onAddHole?: FloorplanActionMenuHandler + onCurve?: FloorplanActionMenuHandler onDuplicate?: FloorplanActionMenuHandler } @@ -84,6 +85,7 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({ > + )} {polygonDraftPolylinePoints && ( diff --git a/packages/editor/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx b/packages/editor/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx index 8c5663ba..af732f91 100644 --- a/packages/editor/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +++ b/packages/editor/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx @@ -125,7 +125,12 @@ export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLay return ( <> {measurements.map((measurement) => ( - + ) : null} @@ -438,8 +438,6 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({ } : undefined } - onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined} - onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined} onPointerDown={ canFocusStairs && stairSelected ? (event) => { @@ -449,6 +447,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({ } : undefined } + onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined} + onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined} pointerEvents={canSelectStairs ? undefined : 'none'} style={canSelectStairs ? { cursor } : undefined} > @@ -456,8 +456,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({ s.isCaptureMode) const isMobile = useIsMobile() if (isMobile) { @@ -229,7 +230,7 @@ export function EditorLayoutV2({ {/* Main content: left column + right column */}
- {sidebarTabs.length > 0 && ( + {!isCaptureMode && sidebarTabs.length > 0 && ( {viewerContent} diff --git a/packages/editor/src/components/editor/first-person-controls.tsx b/packages/editor/src/components/editor/first-person-controls.tsx index a356699f..02e7a01c 100644 --- a/packages/editor/src/components/editor/first-person-controls.tsx +++ b/packages/editor/src/components/editor/first-person-controls.tsx @@ -1311,14 +1311,15 @@ export const FirstPersonControls = () => { {controllerStart && ( { floatSpringK={1200} gravity={9.81} jumpVel={6} + key="first-person-controller" maxRunSpeed={5.5} maxSlope={1.2} maxWalkSpeed={4} paused={isElevatorRideLocked} position={controllerStart.position} - acceleration={26} - airDragFactor={0.3} - deceleration={30} + ref={controllerRef} /> )} @@ -1408,12 +1408,12 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => { {isLocked && (
- +
- - - - + + + +
Click to look around @@ -1448,7 +1448,7 @@ function ControlHint({ label, keys }: { label: string; keys: string[] }) { function InlineControlHint({ label, keyLabel }: { label: string; keyLabel: string }) { return (
- + {label} diff --git a/packages/editor/src/components/editor/first-person/build-collider-world.ts b/packages/editor/src/components/editor/first-person/build-collider-world.ts index a908d3e0..fa5b563f 100644 --- a/packages/editor/src/components/editor/first-person/build-collider-world.ts +++ b/packages/editor/src/components/editor/first-person/build-collider-world.ts @@ -1,7 +1,7 @@ import { - getGarageVisibleOpeningRatio, type AnyNodeId, type DoorNode, + getGarageVisibleOpeningRatio, isOperationDoorType, sceneRegistry, useInteractive, @@ -97,11 +97,11 @@ function shouldSkipColliderNode(nodeId: string, type: (typeof COLLIDER_NODE_TYPE const node = useScene.getState().nodes[nodeId as AnyNodeId] if (!node || node.type !== 'door') return false - if (node.openingKind === 'opening') return true - if (!node.segments.length) return true - return node.segments.every((segment) => segment.type === 'empty') + if (node.openingKind === 'opening') return true + + return node.segments.every((segment: { type: string }) => segment.type === 'empty') } function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) { @@ -273,9 +273,7 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider } const mergedGeometry = mergeGeometries(geometries, false) - geometries.forEach((geometry) => { - geometry.dispose() - }) + geometries.forEach((geometry) => geometry.dispose()) if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) { mergedGeometry?.dispose() diff --git a/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx b/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx index 2a3e1e79..8525f4a1 100644 --- a/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx +++ b/packages/editor/src/components/editor/first-person/bvh-ecctrl.tsx @@ -1,15 +1,8 @@ import '../../../three-types' import { TransformControls, useKeyboardControls } from '@react-three/drei' -import { useFrame, useThree, type ThreeElements } from '@react-three/fiber' -import { - Suspense, - forwardRef, - useCallback, - useImperativeHandle, - useMemo, - useRef, -} from 'react' +import { type ThreeElements, useFrame, useThree } from '@react-three/fiber' import type { ReactNode } from 'react' +import { forwardRef, Suspense, useCallback, useImperativeHandle, useMemo, useRef } from 'react' import * as THREE from 'three' import { clamp } from 'three/src/math/MathUtils.js' @@ -156,16 +149,7 @@ const BVHEcctrl = forwardRef( const moveDirRef = useRef(null) const elapsedRef = useRef(0) - function useIsInsideKeyboardControls() { - try { - return !!useKeyboardControls() - } catch { - return false - } - } - - const isInsideKeyboardControls = useIsInsideKeyboardControls() - const [_, getKeys] = isInsideKeyboardControls ? useKeyboardControls() : [null, null] + const [, getKeys] = useKeyboardControls() const presetKeys = { forward: false, backward: false, @@ -222,11 +206,11 @@ const BVHEcctrl = forwardRef( const scaledContactRadiusVec = useRef(new THREE.Vector3()) const deltaDist = useRef(new THREE.Vector3()) const currSlopeAngle = useRef(0) - const localMinDistance = useRef(Infinity) + const localMinDistance = useRef(Number.POSITIVE_INFINITY) const localClosestPoint = useRef(new THREE.Vector3()) const localHitNormal = useRef(new THREE.Vector3()) const triNormal = useRef(new THREE.Vector3()) - const globalMinDistance = useRef(Infinity) + const globalMinDistance = useRef(Number.POSITIVE_INFINITY) const globalClosestPoint = useRef(new THREE.Vector3()) const triHitPoint = useRef(new THREE.Vector3()) const segHitPoint = useRef(new THREE.Vector3()) @@ -286,7 +270,13 @@ const BVHEcctrl = forwardRef( const moving = currentLinVel.current.lengthSq() > 1e-6 const platformIsMoving = totalPlatformDeltaPos.current.lengthSq() > 1e-6 - if (!moving && isOnGround.current && !jump && !isOnMovingPlatform.current && !platformIsMoving) { + if ( + !moving && + isOnGround.current && + !jump && + !isOnMovingPlatform.current && + !platformIsMoving + ) { idleTime.current += delta if (idleTime.current > sleepTimeout) isSleeping.current = true } else { @@ -340,7 +330,10 @@ const BVHEcctrl = forwardRef( upAxis.current, ) characterModelTargetQuat.current.setFromRotationMatrix(characterModelLookMatrix.current) - characterModelRef.current.quaternion.slerp(characterModelTargetQuat.current, delta * turnSpeed) + characterModelRef.current.quaternion.slerp( + characterModelTargetQuat.current, + delta * turnSpeed, + ) } const maxSpeed = run ? maxRunSpeed : maxWalkSpeed @@ -358,18 +351,33 @@ const BVHEcctrl = forwardRef( ) currentLinVel.current.add(deltaLinVel.current) } else if (isOnGround.current) { - deltaLinVel.current.copy(currentLinVelOnPlane.current).clampLength(0, deceleration * friction * delta) + deltaLinVel.current + .copy(currentLinVelOnPlane.current) + .clampLength(0, deceleration * friction * delta) currentLinVel.current.sub(deltaLinVel.current) } }, - [acceleration, airDragFactor, counterAccFactor, deceleration, maxRunSpeed, maxWalkSpeed, turnSpeed, characterOrigin], + [ + acceleration, + airDragFactor, + counterAccFactor, + deceleration, + maxRunSpeed, + maxWalkSpeed, + turnSpeed, + characterOrigin, + ], ) const updateSegmentBBox = useCallback(() => { if (!characterGroupRef.current) return - characterSegment.current.start.set(0, capsuleLength / 2, 0).add(characterGroupRef.current.position) - characterSegment.current.end.set(0, -capsuleLength / 2, 0).add(characterGroupRef.current.position) + characterSegment.current.start + .set(0, capsuleLength / 2, 0) + .add(characterGroupRef.current.position) + characterSegment.current.end + .set(0, -capsuleLength / 2, 0) + .add(characterGroupRef.current.position) characterBbox.current .makeEmpty() @@ -394,11 +402,18 @@ const BVHEcctrl = forwardRef( const collisionCheck = useCallback( (mesh: THREE.Mesh, originMatrix: THREE.Matrix4, delta: number) => { - if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeCollisionCheck) return + if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeCollisionCheck) + return - originMatrix.decompose(contactTempPos.current, contactTempQuat.current, contactTempScale.current) + originMatrix.decompose( + contactTempPos.current, + contactTempQuat.current, + contactTempScale.current, + ) collideInvertMatrix.current.copy(originMatrix).invert() - localCharacterSegment.current.copy(characterSegment.current).applyMatrix4(collideInvertMatrix.current) + localCharacterSegment.current + .copy(characterSegment.current) + .applyMatrix4(collideInvertMatrix.current) scaledContactRadiusVec.current.set( capsuleRadius / contactTempScale.current.x, @@ -445,7 +460,10 @@ const BVHEcctrl = forwardRef( contactDepth.current = capsuleRadius - capsuleContactPoint.current.distanceTo(triContactPoint.current) - accumulatedContactNormal.current.addScaledVector(contactNormal.current, contactDepth.current) + accumulatedContactNormal.current.addScaledVector( + contactNormal.current, + contactDepth.current, + ) accumulatedContactPoint.current.add(triContactPoint.current) totalDepth.current += contactDepth.current triangleCount.current += 1 @@ -492,14 +510,16 @@ const BVHEcctrl = forwardRef( const floatingCheck = useCallback( (mesh: THREE.Mesh, originMatrix: THREE.Matrix4) => { - if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeFloatHit) return + if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeFloatHit) return originMatrix.decompose(floatTempPos.current, floatTempQuat.current, floatTempScale.current) floatInvertMatrix.current.copy(originMatrix).invert() floatNormalInverseMatrix.current.getNormalMatrix(floatInvertMatrix.current) floatNormalMatrix.current.getNormalMatrix(originMatrix) - localFloatSensorSegment.current.copy(floatSensorSegment.current).applyMatrix4(floatInvertMatrix.current) + localFloatSensorSegment.current + .copy(floatSensorSegment.current) + .applyMatrix4(floatInvertMatrix.current) localFloatSensorBboxExpendPoint.current .copy(floatSensorBboxExpendPoint.current) .applyMatrix4(floatInvertMatrix.current) @@ -517,20 +537,33 @@ const BVHEcctrl = forwardRef( localFloatSensorBbox.current.min.addScaledVector(scaledFloatRadiusVec.current, -1) localFloatSensorBbox.current.max.add(scaledFloatRadiusVec.current) - localMinDistance.current = Infinity - localClosestPoint.current.set(Infinity, Infinity, Infinity) + localMinDistance.current = Number.POSITIVE_INFINITY + localClosestPoint.current.set( + Number.POSITIVE_INFINITY, + Number.POSITIVE_INFINITY, + Number.POSITIVE_INFINITY, + ) mesh.geometry.boundsTree.shapecast({ intersectsBounds: (box) => box.intersectsBox(localFloatSensorBbox.current), intersectsTriangle: (tri) => { - tri.closestPointToSegment(localFloatSensorSegment.current, triHitPoint.current, segHitPoint.current) - localUpAxis.current.copy(upAxis.current).applyMatrix3(floatNormalInverseMatrix.current).normalize() + tri.closestPointToSegment( + localFloatSensorSegment.current, + triHitPoint.current, + segHitPoint.current, + ) + localUpAxis.current + .copy(upAxis.current) + .applyMatrix3(floatNormalInverseMatrix.current) + .normalize() deltaHit.current.subVectors(triHitPoint.current, localFloatSensorSegment.current.start) deltaHit.current.divide(scaledFloatRadiusVec.current) const totalLengthSq = deltaHit.current.lengthSq() const dot = deltaHit.current.dot(localUpAxis.current) - const verticalLength = Math.abs(dot) / ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius) + const verticalLength = + Math.abs(dot) / + ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius) const horizontalLength = Math.sqrt(Math.max(0, totalLengthSq - dot * dot)) if (horizontalLength < 1 && verticalLength < 1) { @@ -560,9 +593,14 @@ const BVHEcctrl = forwardRef( const handleFloatingResponse = useCallback( (meshes: THREE.Mesh[], jump: boolean, delta: number) => { if (meshes.length === 0) return + let shouldJump = jump - globalMinDistance.current = Infinity - globalClosestPoint.current.set(Infinity, Infinity, Infinity) + globalMinDistance.current = Number.POSITIVE_INFINITY + globalClosestPoint.current.set( + Number.POSITIVE_INFINITY, + Number.POSITIVE_INFINITY, + Number.POSITIVE_INFINITY, + ) floatHitNormal.current.set(0, 1, 0) isOnGround.current = false totalPlatformDeltaPos.current.set(0, 0, 0) @@ -574,7 +612,11 @@ const BVHEcctrl = forwardRef( } } - if (floatCheckType !== 'SHAPECAST' && floatRaycastCandidates.length > 0 && globalMinDistance.current === Infinity) { + if ( + floatCheckType !== 'SHAPECAST' && + floatRaycastCandidates.length > 0 && + globalMinDistance.current === Number.POSITIVE_INFINITY + ) { floatRaycaster.current.ray.origin.copy(floatSensorSegment.current.start) floatRaycaster.current.ray.direction.copy(gravityDir.current) const hits = floatRaycaster.current.intersectObjects(floatRaycastCandidates, false) @@ -582,23 +624,28 @@ const BVHEcctrl = forwardRef( if (hit?.point) { globalClosestPoint.current.copy(hit.point) if (hit.face) { - floatHitNormal.current.copy(hit.face.normal).transformDirection(hit.object.matrixWorld).normalize() + floatHitNormal.current + .copy(hit.face.normal) + .transformDirection(hit.object.matrixWorld) + .normalize() } } } - if (globalClosestPoint.current.x === Infinity) return + if (globalClosestPoint.current.x === Number.POSITIVE_INFINITY) return - relativeHitPoint.current.copy(globalClosestPoint.current).sub(floatSensorSegment.current.start) + relativeHitPoint.current + .copy(globalClosestPoint.current) + .sub(floatSensorSegment.current.start) const currentDistance = relativeHitPoint.current.length() currSlopeAngle.current = floatHitNormal.current.angleTo(upAxis.current) if (currentDistance < floatHeight + capsuleRadius) { isOnGround.current = true - jump = false + shouldJump = false } - if (!jump) { + if (!shouldJump) { const displacement = floatHeight + capsuleRadius - currentDistance const velocityOnHitNormal = currentLinVel.current.dot(floatHitNormal.current) const springForce = displacement * floatSpringK @@ -608,7 +655,17 @@ const BVHEcctrl = forwardRef( currentLinVel.current.addScaledVector(floatHitNormal.current, (totalForce / mass) * delta) } }, - [capsuleRadius, floatCheckType, floatDampingC, floatHeight, floatRaycastCandidates, floatSpringK, floatingCheck, gravity, mass], + [ + capsuleRadius, + floatCheckType, + floatDampingC, + floatHeight, + floatRaycastCandidates, + floatSpringK, + floatingCheck, + gravity, + mass, + ], ) const updateCharacterWithPlatform = useCallback(() => { @@ -646,8 +703,14 @@ const BVHEcctrl = forwardRef( ) const resetLinVel = useCallback(() => currentLinVel.current.set(0, 0, 0), []) - const addLinVel = useCallback((velocity: THREE.Vector3) => currentLinVel.current.add(velocity), []) - const setLinVel = useCallback((velocity: THREE.Vector3) => currentLinVel.current.copy(velocity), []) + const addLinVel = useCallback( + (velocity: THREE.Vector3) => currentLinVel.current.add(velocity), + [], + ) + const setLinVel = useCallback( + (velocity: THREE.Vector3) => currentLinVel.current.copy(velocity), + [], + ) const setMovement = useCallback((movement: MovementInput) => { if (movement.forward !== undefined) forwardState.current = movement.forward if (movement.backward !== undefined) backwardState.current = movement.backward @@ -682,7 +745,9 @@ const BVHEcctrl = forwardRef( debugRaySensorEnd.current?.position.copy(floatSensorSegment.current.end) standPointRef.current?.position.copy(globalClosestPoint.current) if (characterGroupRef.current) { - lookDirRef.current?.position.copy(characterGroupRef.current.position).addScaledVector(upAxis.current, 0.7) + lookDirRef.current?.position + .copy(characterGroupRef.current.position) + .addScaledVector(upAxis.current, 0.7) } lookDirRef.current?.lookAt(lookDirRef.current.position.clone().add(camProjDir.current)) inputDirRef.current?.position.copy(characterSegment.current.end) @@ -698,13 +763,13 @@ const BVHEcctrl = forwardRef( if (paused || elapsedRef.current < delay) return const deltaTime = Math.min(1 / 45, delta) * slowMotionFactor - const keys = isInsideKeyboardControls && getKeys ? getKeys() : presetKeys - const forward = forwardState.current || keys.forward - const backward = backwardState.current || keys.backward - const leftward = leftwardState.current || keys.leftward - const rightward = rightwardState.current || keys.rightward - const run = runState.current || keys.run - const jump = jumpState.current || keys.jump + const keys = getKeys() ?? presetKeys + const forward = forwardState.current || (keys.forward ?? false) + const backward = backwardState.current || (keys.backward ?? false) + const leftward = leftwardState.current || (keys.leftward ?? false) + const rightward = rightwardState.current || (keys.rightward ?? false) + const run = runState.current || (keys.run ?? false) + const jump = jumpState.current || (keys.jump ?? false) setInputDirection({ forward, @@ -740,7 +805,7 @@ const BVHEcctrl = forwardRef( return ( - + {debug && ( @@ -777,8 +842,8 @@ const BVHEcctrl = forwardRef( - - + + diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx old mode 100755 new mode 100644 index b58acf7a..b9fb6bda --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -444,7 +444,11 @@ export function FloatingActionMenu() { ? handleDuplicate : undefined } - onMove={node && !DELETE_ONLY_TYPES.includes(node.type) ? handleMove : undefined} + onMove={ + node && node.type !== 'wall' && !DELETE_ONLY_TYPES.includes(node.type) + ? handleMove + : undefined + } onPointerDown={(e) => e.stopPropagation()} onPointerUp={(e) => e.stopPropagation()} /> diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index 8c04eb72..6ceb9506 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -43,6 +43,7 @@ import { useLiveNodeOverrides, useLiveTransforms, useScene, + WallNode as WallNodeSchema, type WallNode, WindowNode, ZoneNode as ZoneNodeSchema, @@ -4213,7 +4214,6 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ d={path} fill={isDeleteHovered ? palette.deleteFill : palette.slabFill} fillRule="evenodd" - opacity={slabFillOpacity} onClick={ canSelectSlabs ? (event) => { @@ -4232,9 +4232,10 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ } onPointerEnter={canSelectSlabs ? () => onSlabHoverChange(slab.id) : undefined} onPointerLeave={canSelectSlabs ? () => onSlabHoverChange(null) : undefined} + opacity={slabFillOpacity} pointerEvents={canSelectSlabs ? undefined : 'none'} - style={canSelectSlabs ? { cursor: EDITOR_CURSOR } : undefined} stroke="none" + style={canSelectSlabs ? { cursor: EDITOR_CURSOR } : undefined} /> {isSelected && !isDeleteHovered ? ( { @@ -4310,9 +4310,10 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ canSelectCeilings ? () => onCeilingHoverChange(ceiling.id) : undefined } onPointerLeave={canSelectCeilings ? () => onCeilingHoverChange(null) : undefined} + opacity={ceilingFillOpacity} pointerEvents={canSelectCeilings ? undefined : 'none'} - style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined} stroke="none" + style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined} /> {isSelected && !isDeleteHovered ? ( ) : null} @@ -4548,8 +4549,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({ {canSelectGeometry && ( @@ -6513,6 +6514,12 @@ function FloorplanItemImage({ }) { const resolvedUrl = useResolvedAssetUrl(url) if (!resolvedUrl) return null + // The PNG is captured with the modal's top-down camera (default up = +Y), + // so its pixel-right is world +X and pixel-up is world -Z. The plan SVG + // negates both axes (`toSvgX(v) = -v`, `toSvgY(v) = -v`), which together + // are a 180° rotation — so the captured image lands upside-down / + // mirrored when overlaid as-is. Bake that 180° into the image transform + // here; the panel / modal previews use the PNG directly and stay correct. const rotationDeg = (-rotation * 180) / Math.PI + 180 return ( ) : null} {itemDimensionMeasurements.length > 0 ? ( @@ -6858,8 +6866,8 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({ onEdgePointerDown(nodeId, edgeIndex, event) + : undefined + } pointerEvents="stroke" stroke="transparent" strokeLinecap="round" @@ -7784,11 +7797,6 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({ x2={endSvg.x} y1={startSvg.y} y2={endSvg.y} - onPointerDown={ - onEdgePointerDown - ? (event) => onEdgePointerDown(nodeId, edgeIndex, event) - : undefined - } /> ) @@ -8035,6 +8043,7 @@ export function FloorplanPanel() { const setPhase = useEditor((state) => state.setPhase) const setMovingFenceEndpoint = useEditor((state) => state.setMovingFenceEndpoint) const setMovingNode = useEditor((state) => state.setMovingNode) + const setCurvingWall = useEditor((state) => state.setCurvingWall) const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint) const structureLayer = useEditor((state) => state.structureLayer) const setStructureLayer = useEditor((state) => state.setStructureLayer) @@ -10004,6 +10013,7 @@ export function FloorplanPanel() { selectedWallEntry, wallCurveDraft, ]) + const canCurveSelectedWall = wallCurveHandles.length > 0 const slabVertexHandles = useMemo(() => { if (!shouldShowSlabBoundaryHandles) { return [] @@ -10611,7 +10621,7 @@ export function FloorplanPanel() { zoneVertexDragState != null || isPolygonDraftBuildActive - if (!hasUserAdjustedViewportRef.current && !transientFloorplanFit) { + if (!(hasUserAdjustedViewportRef.current || transientFloorplanFit)) { setViewport((current) => floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport, ) @@ -11864,6 +11874,14 @@ export function FloorplanPanel() { } }, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh]) + useEffect(() => { + if (!hasPendingItemMeshFootprints) { + return + } + + scheduleMovingFloorplanNodeRefresh() + }, [hasPendingItemMeshFootprints, scheduleMovingFloorplanNodeRefresh]) + // Subscribe to the live-transforms store so rotation/position changes that // *don't* go through pointer events still refresh the floorplan — e.g. R/T // keyboard rotation during placement updates `useLiveTransforms` but emits @@ -11879,14 +11897,6 @@ export function FloorplanPanel() { return unsubscribe }, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh]) - useEffect(() => { - if (!hasPendingItemMeshFootprints) { - return - } - - scheduleMovingFloorplanNodeRefresh() - }, [hasPendingItemMeshFootprints, scheduleMovingFloorplanNodeRefresh]) - useEffect(() => { if (!(movingNode?.type === 'door' || movingNode?.type === 'window')) { return @@ -14985,6 +14995,57 @@ export function FloorplanPanel() { }, [selectedWallEntry, setMovingNode, setSelection], ) + const duplicateSelectedWall = useCallback(() => { + const wall = selectedWallEntry?.wall + if (!wall?.parentId) { + return + } + + sfxEmitter.emit('sfx:item-pick') + + const cloned = structuredClone(wall) as Record + delete cloned.id + cloned.children = [] + cloned.metadata = { + ...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}), + isNew: true, + } + + const temporal = useScene.temporal.getState() + temporal.pause() + try { + const duplicate = WallNodeSchema.parse(cloned) + useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) + setMovingNode(duplicate) + setSelection({ selectedIds: [] }) + } catch (error) { + console.error('Failed to duplicate wall', error) + } finally { + temporal.resume() + } + }, [selectedWallEntry, setMovingNode, setSelection]) + const handleSelectedWallDuplicate = useCallback( + (event: ReactMouseEvent) => { + event.stopPropagation() + duplicateSelectedWall() + }, + [duplicateSelectedWall], + ) + const handleSelectedWallCurve = useCallback( + (event: ReactMouseEvent) => { + event.stopPropagation() + + const wall = selectedWallEntry?.wall + if (!(wall && canCurveSelectedWall)) { + return + } + + sfxEmitter.emit('sfx:item-pick') + setCurvingWall(wall) + setSelection({ selectedIds: [] }) + }, + [canCurveSelectedWall, selectedWallEntry, setCurvingWall, setSelection], + ) const handleSelectedWallDelete = useCallback( (event: ReactMouseEvent) => { event.stopPropagation() @@ -17036,9 +17097,17 @@ export function FloorplanPanel() { site, ]) const hasDuplicatableFloorplanSelection = Boolean( - selectedItemEntry || selectedOpeningEntry || selectedStairEntry || selectedRoofEntry, + selectedItemEntry || + selectedOpeningEntry || + selectedStairEntry || + selectedRoofEntry || + selectedWallEntry, ) const handleDuplicateFloorplanSelection = useCallback(() => { + if (selectedWallEntry) { + duplicateSelectedWall() + return + } if (selectedOpeningEntry) { duplicateSelectedOpening() return @@ -17055,6 +17124,7 @@ export function FloorplanPanel() { duplicateSelectedRoof() } }, [ + duplicateSelectedWall, duplicateSelectedItem, duplicateSelectedOpening, duplicateSelectedRoof, @@ -17063,6 +17133,7 @@ export function FloorplanPanel() { selectedOpeningEntry, selectedRoofEntry, selectedStairEntry, + selectedWallEntry, ]) const activeDraftAnchorPoint = referenceScaleDraft?.start ?? @@ -17160,17 +17231,13 @@ export function FloorplanPanel() { onDuplicate: handleSelectedItemDuplicate, onMove: handleSelectedItemMove, }} + offsetY={FLOORPLAN_ACTION_MENU_OFFSET_Y} opening={{ position: selectedOpeningActionMenuPosition, onDelete: handleSelectedOpeningDelete, onDuplicate: handleSelectedOpeningDuplicate, onMove: handleSelectedOpeningMove, }} - spawn={{ - position: selectedSpawnActionMenuPosition, - onDelete: handleSelectedSpawnDelete, - onMove: handleSelectedSpawnMove, - }} roof={{ position: selectedRoofActionMenuPosition, onDelete: handleSelectedRoofDelete, @@ -17185,6 +17252,11 @@ export function FloorplanPanel() { : handleSelectedSlabDelete, onMove: selectedSlabEditingHole ? handleSelectedSlabHoleMove : handleSelectedSlabMove, }} + spawn={{ + position: selectedSpawnActionMenuPosition, + onDelete: handleSelectedSpawnDelete, + onMove: handleSelectedSpawnMove, + }} stair={{ position: selectedStairActionMenuPosition, onDelete: handleSelectedStairDelete, @@ -17193,10 +17265,11 @@ export function FloorplanPanel() { }} wall={{ position: selectedWallActionMenuPosition, + onCurve: canCurveSelectedWall ? handleSelectedWallCurve : undefined, onDelete: handleSelectedWallDelete, + onDuplicate: handleSelectedWallDuplicate, onMove: handleSelectedWallMove, }} - offsetY={FLOORPLAN_ACTION_MENU_OFFSET_Y} /> {referenceScaleDraft && ( @@ -17229,7 +17302,7 @@ export function FloorplanPanel() {
-
+
Drawn line
@@ -17391,8 +17464,8 @@ export function FloorplanPanel() { @@ -17529,8 +17602,8 @@ export function FloorplanPanel() {
{hints.map((hint) => ( @@ -585,6 +588,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({ <> {!isFirstPersonMode && } {!(isVersionPreviewMode || isFirstPersonMode) && } + {!(isVersionPreviewMode || isFirstPersonMode) && } {!(isVersionPreviewMode || isFirstPersonMode) && } {!(isVersionPreviewMode || isFirstPersonMode) && } {!isFirstPersonMode && } @@ -753,7 +757,7 @@ function PaintCursorLayer({ (activePaintMaterial.material !== undefined || activePaintMaterial.materialPreset !== undefined), ) - const label = !hasMaterial ? 'Choose material' : `Paint ${activePaintTarget}` + const label = hasMaterial ? `Paint ${activePaintTarget}` : 'Choose material' const icon = 'mdi:format-color-fill' useLayoutEffect(() => { @@ -786,16 +790,16 @@ function PaintCursorLayer({ const ViewerCanvas = memo(function ViewerCanvas({ isVersionPreviewMode, isLoading, + isFirstPersonMode, hasLoadedInitialScene, showLoader, - isFirstPersonMode, onThumbnailCapture, }: { isVersionPreviewMode: boolean isLoading: boolean + isFirstPersonMode: boolean hasLoadedInitialScene: boolean showLoader: boolean - isFirstPersonMode: boolean onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void }) { const viewMode = useEditor((s) => s.viewMode) @@ -837,7 +841,7 @@ const ViewerCanvas = memo(function ViewerCanvas({ window.removeEventListener('pointermove', handlePointerMove) window.removeEventListener('pointerup', handlePointerUp) } - }, [setFloorplanPaneRatio]) + }, []) useEffect(() => { setIsCameraControlsHintVisible(!readCameraControlsHintDismissed()) @@ -940,7 +944,9 @@ export default function Editor({ commandPaletteEmptyAction, }: EditorProps) { const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode) + useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode }) + const { isLoadingSceneRef } = useAutoSave({ onSave, onDirty, @@ -951,8 +957,7 @@ export default function Editor({ const [isSceneLoading, setIsSceneLoading] = useState(false) const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false) const isPreviewMode = useEditor((s) => s.isPreviewMode) - const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId) - const wasFirstPersonModeRef = useRef(isFirstPersonMode) + const isCaptureMode = useEditor((s) => s.isCaptureMode) const sidebarWidth = useSidebarStore((s) => s.width) const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed) @@ -970,39 +975,6 @@ export default function Editor({ } }, [projectId]) - useEffect(() => { - const wasFirstPersonMode = wasFirstPersonModeRef.current - wasFirstPersonModeRef.current = isFirstPersonMode - - if (isFirstPersonMode && !wasFirstPersonMode) { - const viewer = useViewer.getState() - firstPersonPreviousLevelRef.current = viewer.selection.levelId - viewer.setCameraMode('perspective') - viewer.setWallMode('up') - viewer.setWalkthroughMode(true) - viewer.setSelection({ selectedIds: [], zoneId: null }) - return - } - - if (!(wasFirstPersonMode && !isFirstPersonMode)) return - - const viewer = useViewer.getState() - const previousLevelId = firstPersonPreviousLevelRef.current - firstPersonPreviousLevelRef.current = null - viewer.setWalkthroughMode(false) - - if (!previousLevelId) return - - const previousLevelNode = useScene.getState().nodes[previousLevelId] - if (previousLevelNode?.type === 'level') { - viewer.setSelection({ - levelId: previousLevelId, - zoneId: null, - selectedIds: [], - }) - } - }, [isFirstPersonMode]) - // Load scene on mount (or when onLoad identity changes, e.g. project switch) useEffect(() => { let cancelled = false @@ -1064,6 +1036,42 @@ export default function Editor({ const showLoader = isLoading || isSceneLoading + const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId) + const wasFirstPersonModeRef = useRef(isFirstPersonMode) + + useEffect(() => { + const wasFirstPersonMode = wasFirstPersonModeRef.current + wasFirstPersonModeRef.current = isFirstPersonMode + + if (isFirstPersonMode && !wasFirstPersonMode) { + const viewer = useViewer.getState() + firstPersonPreviousLevelRef.current = viewer.selection.levelId + viewer.setCameraMode('perspective') + viewer.setWallMode('up') + viewer.setWalkthroughMode(true) + viewer.setSelection({ selectedIds: [], zoneId: null }) + return + } + + if (!(wasFirstPersonMode && !isFirstPersonMode)) return + + const viewer = useViewer.getState() + const previousLevelId = firstPersonPreviousLevelRef.current + firstPersonPreviousLevelRef.current = null + viewer.setWalkthroughMode(false) + + if (!previousLevelId) return + + const previousLevelNode = useScene.getState().nodes[previousLevelId] + if (previousLevelNode?.type === 'level') { + viewer.setSelection({ + levelId: previousLevelId, + zoneId: null, + selectedIds: [], + }) + } + }, [isFirstPersonMode]) + const previewViewerContent = ( @@ -1135,21 +1143,24 @@ export default function Editor({ navbarSlot={navbarSlot} overlays={ <> - - {!isVersionPreviewMode && ( + {!isCaptureMode && } + {!(isVersionPreviewMode || isCaptureMode) && (
)} - {!isVersionPreviewMode && ( + {!(isVersionPreviewMode || isCaptureMode) && (
)} -
- -
+ {!isCaptureMode && ( +
+ +
+ )} {viewerBanner} + {projectId ? : null} } renderTabContent={renderTabContent} @@ -1159,14 +1170,14 @@ export default function Editor({ viewerToolbarLeft={viewerToolbarLeft} viewerToolbarRight={viewerToolbarRight} /> + + {/* First-person overlay — rendered on top of normal layout */} {isFirstPersonMode && (
useEditor.getState().setFirstPersonMode(false)} />
)} - - )} @@ -1222,6 +1233,12 @@ export default function Editor({
+ {/* First-person overlay — rendered on top of normal layout */} + {isFirstPersonMode && ( +
+ useEditor.getState().setFirstPersonMode(false)} /> +
+ )} )}
diff --git a/packages/editor/src/components/editor/selection-manager.tsx b/packages/editor/src/components/editor/selection-manager.tsx old mode 100755 new mode 100644 index ab5d0d2f..1927ed51 --- a/packages/editor/src/components/editor/selection-manager.tsx +++ b/packages/editor/src/components/editor/selection-manager.tsx @@ -343,7 +343,7 @@ function applySingleSurfacePaintPreview( if (node.type === 'ceiling') { const root = getRegisteredMesh(node.id) const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined - if (!root || !overlay) return null + if (!(root && overlay)) return null const previewColor = getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ?? @@ -1011,7 +1011,6 @@ export const SelectionManager = () => { 'roof-segment', 'stair', 'stair-segment', - 'spawn', 'window', 'door', 'zone', @@ -1408,6 +1407,7 @@ export const SelectionManager = () => { 'roof-segment', 'stair', 'stair-segment', + 'spawn', 'window', 'door', 'zone', @@ -1652,6 +1652,7 @@ const EditorOutlinerSync = () => { const previewSelectedIds = useViewer((s) => s.previewSelectedIds) const hoveredId = useViewer((s) => s.hoveredId) const outliner = useViewer((s) => s.outliner) + const nodes = useScene((s) => s.nodes) useEffect(() => { let idsToHighlight: string[] = [] @@ -1688,16 +1689,21 @@ const EditorOutlinerSync = () => { // 2. Sync with the imperative outliner arrays (mutate in place to keep references) outliner.selectedObjects.length = 0 for (const id of idsToHighlight) { + if (!nodes[id as AnyNodeId]) continue const obj = sceneRegistry.nodes.get(id) if (obj?.parent) outliner.selectedObjects.push(obj) } outliner.hoveredObjects.length = 0 if (hoveredId) { - const obj = sceneRegistry.nodes.get(hoveredId) - if (obj?.parent) outliner.hoveredObjects.push(obj) + if (!nodes[hoveredId as AnyNodeId]) { + useViewer.setState({ hoveredId: null }) + } else { + const obj = sceneRegistry.nodes.get(hoveredId) + if (obj?.parent) outliner.hoveredObjects.push(obj) + } } - }, [phase, previewSelectedIds, selection, hoveredId, outliner]) + }, [phase, previewSelectedIds, selection, hoveredId, outliner, nodes]) return null } diff --git a/packages/editor/src/components/editor/snapshot-capture-overlay.tsx b/packages/editor/src/components/editor/snapshot-capture-overlay.tsx new file mode 100644 index 00000000..d8632595 --- /dev/null +++ b/packages/editor/src/components/editor/snapshot-capture-overlay.tsx @@ -0,0 +1,465 @@ +'use client' + +import { emitter } from '@pascal-app/core' +import { Camera, Check, Crop, Loader2, Maximize2, Monitor, X } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useIsMobile } from '../../hooks/use-mobile' +import { triggerSFX } from '../../lib/sfx-bus' +import useEditor from '../../store/use-editor' + +type CaptureMode = 'standard' | 'viewport' | 'area' +type CaptureState = 'idle' | 'capturing' | 'saved' + +interface DragPoint { + x: number + y: number +} + +interface Drag { + start: DragPoint + end: DragPoint +} + +function getResolution( + mode: CaptureMode, + overlayEl: HTMLDivElement | null, + drag: Drag | null, +): { w: number; h: number } | null { + if (mode === 'standard') return { w: 1920, h: 1080 } + + if (!overlayEl) return null + const rect = overlayEl.getBoundingClientRect() + const dpr = Math.min(window.devicePixelRatio, 1.5) + + if (mode === 'viewport') { + return { w: Math.round(rect.width * dpr), h: Math.round(rect.height * dpr) } + } + + if (mode === 'area' && drag) { + const w = Math.abs(drag.end.x - drag.start.x) + const h = Math.abs(drag.end.y - drag.start.y) + if (w < 4 || h < 4) return null + return { w: Math.round(w * dpr), h: Math.round(h * dpr) } + } + + return null +} + +export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) { + const isCaptureMode = useEditor((s) => s.isCaptureMode) + const setCaptureMode = useEditor((s) => s.setCaptureMode) + const isMobile = useIsMobile() + + const [mode, setMode] = useState('standard') + const [drag, setDrag] = useState(null) + const [isDragging, setIsDragging] = useState(false) + const [captureState, setCaptureState] = useState('idle') + const overlayRef = useRef(null) + + // Dismiss on Esc + useEffect(() => { + if (!isCaptureMode) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setCaptureMode(false) + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [isCaptureMode, setCaptureMode]) + + // Reset local state when entering capture mode + useEffect(() => { + if (isCaptureMode) { + setMode('standard') + setDrag(null) + setIsDragging(false) + setCaptureState('idle') + } + }, [isCaptureMode]) + + // Listen for snapshot saved to show feedback then exit + useEffect(() => { + const handler = () => { + setCaptureState('saved') + setTimeout(() => { + setCaptureMode(false) + setCaptureState('idle') + }, 1500) + } + emitter.on('snapshot:saved', handler) + return () => emitter.off('snapshot:saved', handler) + }, [setCaptureMode]) + + const dismiss = useCallback(() => setCaptureMode(false), [setCaptureMode]) + + // Tracks whether the active drag is a "move entire rect" gesture + const moveStartRef = useRef<{ pt: DragPoint; drag: Drag } | null>(null) + + // Area drag handlers — relative to the overlay container + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + if (mode !== 'area' || captureState !== 'idle') return + e.preventDefault() + const rect = overlayRef.current!.getBoundingClientRect() + const pt = { x: e.clientX - rect.left, y: e.clientY - rect.top } + + // If clicking inside an existing selection → move mode + if (drag) { + const x0 = Math.min(drag.start.x, drag.end.x) + const y0 = Math.min(drag.start.y, drag.end.y) + const x1 = Math.max(drag.start.x, drag.end.x) + const y1 = Math.max(drag.start.y, drag.end.y) + if (pt.x >= x0 && pt.x <= x1 && pt.y >= y0 && pt.y <= y1) { + moveStartRef.current = { pt, drag } + setIsDragging(true) + ;(e.target as HTMLElement).setPointerCapture(e.pointerId) + return + } + } + + // Outside / no selection → start new drag + moveStartRef.current = null + setDrag({ start: pt, end: pt }) + setIsDragging(true) + ;(e.target as HTMLElement).setPointerCapture(e.pointerId) + }, + [mode, captureState, drag], + ) + + const onPointerMove = useCallback( + (e: React.PointerEvent) => { + if (!isDragging) return + const rect = overlayRef.current!.getBoundingClientRect() + const pt = { + x: Math.max(0, Math.min(e.clientX - rect.left, rect.width)), + y: Math.max(0, Math.min(e.clientY - rect.top, rect.height)), + } + if (moveStartRef.current) { + // Move mode: translate the whole rect by the delta + const { pt: origin, drag: snapshot } = moveStartRef.current + const dx = pt.x - origin.x + const dy = pt.y - origin.y + setDrag({ + start: { x: snapshot.start.x + dx, y: snapshot.start.y + dy }, + end: { x: snapshot.end.x + dx, y: snapshot.end.y + dy }, + }) + } else { + setDrag((d) => (d ? { start: d.start, end: pt } : null)) + } + }, + [isDragging], + ) + + const onPointerUp = useCallback(() => { + const wasMoving = moveStartRef.current !== null + setIsDragging(false) + moveStartRef.current = null + // Clear the rect if the user just clicked without drawing (not a move gesture) + if (!wasMoving) { + setDrag((d) => { + if (!d) return null + const w = Math.abs(d.end.x - d.start.x) + const h = Math.abs(d.end.y - d.start.y) + return w < 4 && h < 4 ? null : d + }) + } + }, []) + + // Corner-handle resize: re-anchor to the opposite corner then reuse the same drag machinery + const onCornerPointerDown = useCallback( + (e: React.PointerEvent, cornerIndex: number) => { + if (captureState !== 'idle' || !drag) return + e.stopPropagation() + e.preventDefault() + moveStartRef.current = null + const x0 = Math.min(drag.start.x, drag.end.x) + const y0 = Math.min(drag.start.y, drag.end.y) + const x1 = Math.max(drag.start.x, drag.end.x) + const y1 = Math.max(drag.start.y, drag.end.y) + // anchor = opposite corner; dragged = current corner + const corners: [DragPoint, DragPoint][] = [ + [ + { x: x1, y: y1 }, + { x: x0, y: y0 }, + ], // TL → anchor BR + [ + { x: x0, y: y1 }, + { x: x1, y: y0 }, + ], // TR → anchor BL + [ + { x: x1, y: y0 }, + { x: x0, y: y1 }, + ], // BL → anchor TR + [ + { x: x0, y: y0 }, + { x: x1, y: y1 }, + ], // BR → anchor TL + ] + const [anchor, current] = corners[cornerIndex]! + setDrag({ start: anchor, end: current }) + setIsDragging(true) + }, + [captureState, drag], + ) + + const handleCapture = useCallback(() => { + if (captureState !== 'idle') return + + let cropRegion: { x: number; y: number; width: number; height: number } | undefined + if (mode === 'area' && drag && overlayRef.current) { + const rect = overlayRef.current.getBoundingClientRect() + const x0 = Math.min(drag.start.x, drag.end.x) + const y0 = Math.min(drag.start.y, drag.end.y) + const w = Math.abs(drag.end.x - drag.start.x) + const h = Math.abs(drag.end.y - drag.start.y) + cropRegion = { + x: x0 / rect.width, + y: y0 / rect.height, + width: w / rect.width, + height: h / rect.height, + } + } + + setCaptureState('capturing') + triggerSFX('sfx:snapshot-capture') + emitter.emit('camera-controls:generate-thumbnail', { + projectId, + captureMode: mode, + cropRegion, + }) + }, [captureState, mode, drag, projectId]) + + if (!isCaptureMode) return null + + const resolution = getResolution(mode, overlayRef.current, drag) + + // Area selection rect (CSS px, relative to overlay) + const selectionStyle = + mode === 'area' && drag + ? { + left: Math.min(drag.start.x, drag.end.x), + top: Math.min(drag.start.y, drag.end.y), + width: Math.abs(drag.end.x - drag.start.x), + height: Math.abs(drag.end.y - drag.start.y), + } + : null + + const hasSelection = + selectionStyle != null && selectionStyle.width > 3 && selectionStyle.height > 3 + + const captureDisabled = captureState !== 'idle' || (mode === 'area' && !hasSelection) + + return ( +
+ {/* Area mode: dim layer + crosshair cursor + drag-to-select */} + {mode === 'area' && ( +
{ + onPointerMove(e) + // Update cursor: 'move' when hovering inside an existing selection + if (!isDragging && drag && overlayRef.current) { + const rect = overlayRef.current.getBoundingClientRect() + const px = e.clientX - rect.left + const py = e.clientY - rect.top + const x0 = Math.min(drag.start.x, drag.end.x) + const y0 = Math.min(drag.start.y, drag.end.y) + const x1 = Math.max(drag.start.x, drag.end.x) + const y1 = Math.max(drag.start.y, drag.end.y) + e.currentTarget.style.cursor = + px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair' + } + }} + onPointerUp={onPointerUp} + style={{ cursor: 'crosshair' }} + > + {/* "No selection" hint */} + {!selectionStyle && ( +
+ + Drag the area you want to capture + +
+ )} + + {/* Selection rect */} + {selectionStyle && ( +
+ {/* Corner handles */} + {( + [ + { pos: { top: -5, left: -5 }, cursor: 'nwse-resize' }, + { pos: { top: -5, right: -5 }, cursor: 'nesw-resize' }, + { pos: { bottom: -5, left: -5 }, cursor: 'nesw-resize' }, + { pos: { bottom: -5, right: -5 }, cursor: 'nwse-resize' }, + ] as const + ).map(({ pos, cursor }, i) => ( +
onCornerPointerDown(e, i)} + style={{ + position: 'absolute', + width: 10, + height: 10, + borderRadius: '50%', + background: 'white', + boxShadow: '0 1px 4px rgba(0,0,0,0.4)', + pointerEvents: 'auto', + cursor, + ...pos, + }} + /> + ))} +
+ )} +
+ )} + + {/* Top-right dismiss button (icon-only on mobile) */} +
+ +
+ + {/* Bottom-center mode toolbar */} +
+ {(() => { + const modeButtons = ( + <> + } + label="Standard" + onClick={() => { + setMode('standard') + setDrag(null) + }} + /> + } + label="Viewport" + onClick={() => { + setMode('viewport') + setDrag(null) + }} + /> + } + label="Area" + onClick={() => setMode('area')} + /> + + ) + + const resolutionDisplay = ( + + {resolution ? `${resolution.w} × ${resolution.h}` : '—'} + + ) + + const captureButton = ( + + ) + + if (isMobile) { + return ( +
+
{modeButtons}
+
+ {resolutionDisplay} + {captureButton} +
+
+ ) + } + + return ( +
+ {modeButtons} +
+ {resolutionDisplay} +
+ {captureButton} +
+ ) + })()} +
+
+ ) +} + +function ModeButton({ + active, + icon, + label, + badge, + onClick, +}: { + active: boolean + icon: React.ReactNode + label: string + badge?: string + onClick: () => void +}) { + return ( + + ) +} diff --git a/packages/editor/src/components/editor/thumbnail-generator.tsx b/packages/editor/src/components/editor/thumbnail-generator.tsx index 826eb09a..b1135943 100644 --- a/packages/editor/src/components/editor/thumbnail-generator.tsx +++ b/packages/editor/src/components/editor/thumbnail-generator.tsx @@ -1,6 +1,6 @@ 'use client' -import { emitter, sceneRegistry, useScene } from '@pascal-app/core' +import { emitter, sceneRegistry } from '@pascal-app/core' import { SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer' import type { CameraControls } from '@react-three/drei' import { useThree } from '@react-three/fiber' @@ -28,7 +28,6 @@ import { EDITOR_LAYER } from '../../lib/constants' const THUMBNAIL_WIDTH = 1920 const THUMBNAIL_HEIGHT = 1080 -const AUTO_SAVE_DELAY = 10_000 export interface SnapshotCameraData { position: [number, number, number] @@ -49,8 +48,6 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro const mainCamera = useThree((state) => state.camera) const controls = useThree((state) => state.controls) as CameraControls | null const isGenerating = useRef(false) - const debounceTimerRef = useRef | null>(null) - const pendingAutoRef = useRef(false) const onThumbnailCaptureRef = useRef(onThumbnailCapture) const thumbnailCameraRef = useRef(null) @@ -61,7 +58,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro onThumbnailCaptureRef.current = onThumbnailCapture }, [onThumbnailCapture]) - // Build the thumbnail camera, SSGI pipeline, and render target once, reused on every capture. + // Build the thumbnail camera, SSGI pipeline, and render target once — reused on every capture. useEffect(() => { const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000) cam.layers.disable(EDITOR_LAYER) @@ -75,7 +72,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro if (!mounted) return // pass() handles MRT internally for all material types, including custom - // shaders, unlike renderer.setMRT() which crashes on non-NodeMaterials. + // shaders — unlike renderer.setMRT() which crashes on non-NodeMaterials. // pass() also respects camera.layers, so EDITOR_LAYER objects are filtered. const scenePass = pass(scene, cam) scenePass.setMRT( @@ -117,15 +114,15 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro const ao = (denoisePass as any).r const finalOutput = vec4(scenePassColor.rgb.mul(ao), scenePassColor.a) - // FXAA requires a texture node as input, convertToTexture renders finalOutput - // into an intermediate RT so FXAA can sample it with neighbor UV offsets. + // FXAA requires a texture node as input; convertToTexture renders finalOutput + // into an intermediate RT so FXAA can sample it with neighbour UV offsets. const aaOutput = fxaa(convertToTexture(finalOutput)) const pipeline = new RenderPipeline(gl as unknown as WebGPURenderer) pipeline.outputNode = aaOutput pipelineRef.current = pipeline - // Dedicated render target, pipeline outputs here instead of the canvas, + // Dedicated render target — pipeline outputs here instead of the canvas, // so R3F's main render loop can never overwrite our capture. const { width, height } = gl.domElement renderTargetRef.current = new RenderTarget(width, height, { depthBuffer: true }) @@ -176,7 +173,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro thumbnailCamera.aspect = width / height thumbnailCamera.updateProjectionMatrix() - // Capture camera data for snapshot storage. + // Capture camera data for snapshot storage const pos = mainCamera.position let tgt: [number, number, number] | null = null if (controls && 'getTarget' in controls) { @@ -192,7 +189,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro ...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }), } - // For auto-save, snap levels to stacked positions and reset levelMode. + // For auto-save: snap levels to stacked positions and reset levelMode let restoreLevelMode: (() => void) | null = null let restoreLevels: () => void = () => {} if (snapLevels) { @@ -205,7 +202,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro } // Hide scan and guide nodes directly so they are excluded from the - // thumbnail regardless of whether ScanSystem or GuideSystem listeners are + // thumbnail regardless of whether ScanSystem/GuideSystem listeners are // registered. Returns a function that restores the original visibility. const restoreNodeVisibility = (() => { const saved = new Map() @@ -231,7 +228,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro if (pipelineRef.current && renderTargetRef.current) { const rt = renderTargetRef.current - // Resize RT if the canvas dimensions changed. + // Resize RT if the canvas dimensions changed if (rt.width !== width || rt.height !== height) { rt.setSize(width, height) } @@ -248,7 +245,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro emitter.emit('thumbnail:after-capture', undefined) // Restore level positions, levelMode, and node visibility immediately after the - // render, before the async GPU readback. + // render — before the async GPU readback. restoreLevels() restoreLevelMode?.() restoreNodeVisibility() @@ -339,6 +336,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH) blob = await offscreen.convertToBlob({ type: 'image/png' }) } else { + // Standard: center-crop to 1920×1080 aspect ratio const srcAspect = width / height const dstAspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT let sx = 0, @@ -364,7 +362,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro if (captureMode !== undefined) cameraData.captureMode = captureMode cameraData.resolution = { w: outW, h: outH } } else { - // Fallback: plain render directly to the canvas. + // Fallback: plain render directly to the canvas emitter.emit('thumbnail:before-capture', undefined) gl.render(scene, thumbnailCamera) emitter.emit('thumbnail:after-capture', undefined) @@ -450,10 +448,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro ) // Thumbnail request via emitter. Two call shapes: - // - user-driven capture: `{ projectId, captureMode, cropRegion }`, captures + // - user-driven capture: `{ projectId, captureMode, cropRegion }` — captures // the current pose with the supplied crop. - // - auto-save capture: `{ projectId, snapLevels: true }`, snaps levels to - // their true positions first for a consistent auto-thumbnail angle. + // - host-driven auto-save: `{ projectId, snapLevels: true }` — snaps levels + // to their true positions first for a consistent auto-thumbnail angle. + // The caller owns policy (when to fire, whether the tab is visible). useEffect(() => { if (!onThumbnailCapture) return @@ -469,49 +468,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro return () => emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail) }, [generate, onThumbnailCapture]) - // OSS adaptation: keep local debounced auto-capture behavior because the - // community host-side autosave hook is not part of this repo. - useEffect(() => { - if (!onThumbnailCapture) return - - const triggerNow = () => { - void generate(true) - } - - const scheduleOrDefer = () => { - if (document.visibilityState === 'visible') { - triggerNow() - } else { - pendingAutoRef.current = true - } - } - - const onSceneChange = () => { - if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) - debounceTimerRef.current = setTimeout(scheduleOrDefer, AUTO_SAVE_DELAY) - } - - const onVisibilityChange = () => { - if (document.visibilityState === 'visible' && pendingAutoRef.current) { - pendingAutoRef.current = false - triggerNow() - } - } - - const unsubscribe = useScene.subscribe((state, prevState) => { - if (state.nodes !== prevState.nodes) onSceneChange() - }) - - document.addEventListener('visibilitychange', onVisibilityChange) - - return () => { - if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) - unsubscribe() - document.removeEventListener('visibilitychange', onVisibilityChange) - } - }, [generate, onThumbnailCapture]) - - // Go-to-camera: animate camera to a saved snapshot position or target. + // Go-to-camera: animate camera to a saved snapshot position/target useEffect(() => { const handler = ({ position, diff --git a/packages/editor/src/components/editor/use-floorplan-background-placement.ts b/packages/editor/src/components/editor/use-floorplan-background-placement.ts index a96db822..d6228200 100644 --- a/packages/editor/src/components/editor/use-floorplan-background-placement.ts +++ b/packages/editor/src/components/editor/use-floorplan-background-placement.ts @@ -144,11 +144,11 @@ export function useFloorplanBackgroundPlacement({ emitFloorplanGridEvent('click', snappedPoint, event) setCursorPoint(snappedPoint) - if (!roofDraftStart) { + if (roofDraftStart) { + clearRoofPlacementDraft() + } else { setRoofDraftStart(snappedPoint) setRoofDraftEnd(snappedPoint) - } else { - clearRoofPlacementDraft() } return true } diff --git a/packages/editor/src/components/editor/wall-measurement-label.tsx b/packages/editor/src/components/editor/wall-measurement-label.tsx old mode 100755 new mode 100644 diff --git a/packages/editor/src/components/editor/wall-move-side-handles.tsx b/packages/editor/src/components/editor/wall-move-side-handles.tsx new file mode 100644 index 00000000..43bd9019 --- /dev/null +++ b/packages/editor/src/components/editor/wall-move-side-handles.tsx @@ -0,0 +1,259 @@ +'use client' + +import { + type AnyNodeId, + DEFAULT_WALL_HEIGHT, + getWallThickness, + sceneRegistry, + useScene, + type WallNode, +} from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { createPortal, type ThreeEvent } from '@react-three/fiber' +import { useEffect, useMemo, useState } from 'react' +import { + BufferGeometry, + ConeGeometry, + CylinderGeometry, + DoubleSide, + Float32BufferAttribute, + type Object3D, +} from 'three' +import { sfxEmitter } from '../../lib/sfx-bus' +import useEditor from '../../store/use-editor' + +const HANDLE_OFFSET = 0.42 +const HANDLE_MIN_OFFSET = 0.5 +const HANDLE_MIN_HEIGHT = 0.62 +const HANDLE_TOP_INSET = 0.08 +const ARROW_COLOR = '#8381ed' +const ARROW_HOVER_COLOR = '#a5b4fc' + +type WallMoveHandle = { + direction: [number, number] + key: string + position: [number, number, number] + rotationY: number +} + +function createArrowHandleGeometry() { + const shaft = new CylinderGeometry(0.04, 0.064, 0.25, 36) + const head = new ConeGeometry(0.13, 0.3, 48) + shaft.rotateZ(-Math.PI / 2) + shaft.translate(-0.085, 0, 0) + head.rotateZ(-Math.PI / 2) + head.translate(0.17, 0, 0) + + const positions: number[] = [] + const normals: number[] = [] + const uvs: number[] = [] + + for (const sourceGeometry of [shaft, head]) { + const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry + const position = geometry.getAttribute('position') + const normal = geometry.getAttribute('normal') + const uv = geometry.getAttribute('uv') + + for (let index = 0; index < position.count; index += 1) { + positions.push(position.getX(index), position.getY(index), position.getZ(index)) + normals.push(normal.getX(index), normal.getY(index), normal.getZ(index)) + uvs.push(uv?.getX(index) ?? 0, uv?.getY(index) ?? 0) + } + + if (geometry !== sourceGeometry) { + geometry.dispose() + } + sourceGeometry.dispose() + } + + const geometry = new BufferGeometry() + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)) + geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3)) + geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) + geometry.setAttribute('uv2', new Float32BufferAttribute([...uvs], 2)) + geometry.computeVertexNormals() + geometry.computeBoundingSphere() + return geometry +} + +export function WallMoveSideHandles() { + const selectedIds = useViewer((state) => state.selection.selectedIds) + const mode = useEditor((state) => state.mode) + const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered) + const movingNode = useEditor((state) => state.movingNode) + const movingWallEndpoint = useEditor((state) => state.movingWallEndpoint) + const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint) + const curvingWall = useEditor((state) => state.curvingWall) + const curvingFence = useEditor((state) => state.curvingFence) + + const selectedId = selectedIds.length === 1 ? selectedIds[0] : null + const wall = useScene((state) => { + const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null + return node?.type === 'wall' ? node : null + }) + + const shouldRender = + Boolean(wall) && + !isFloorplanHovered && + mode !== 'delete' && + !movingNode && + !movingWallEndpoint && + !movingFenceEndpoint && + !curvingWall && + !curvingFence + + if (!shouldRender || !wall) return null + + return +} + +function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) { + const [levelObject, setLevelObject] = useState(() => + wall.parentId ? (sceneRegistry.nodes.get(wall.parentId) ?? null) : null, + ) + + useEffect(() => { + let frameId = 0 + + const resolveLevelObject = () => { + const nextLevelObject = wall.parentId + ? (sceneRegistry.nodes.get(wall.parentId) ?? null) + : null + setLevelObject((currentLevelObject) => { + if (currentLevelObject === nextLevelObject) { + return currentLevelObject + } + return nextLevelObject + }) + + if (!nextLevelObject) { + frameId = window.requestAnimationFrame(resolveLevelObject) + } + } + + resolveLevelObject() + + return () => { + if (frameId) { + window.cancelAnimationFrame(frameId) + } + } + }, [wall.parentId]) + + const handles = useMemo(() => getWallMoveHandles(wall), [wall]) + + if (!levelObject || handles.length === 0) return null + + return createPortal( + + {handles.map((handle) => ( + + ))} + , + levelObject, + ) +} + +function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMoveHandle }) { + const [isHovered, setIsHovered] = useState(false) + const arrowGeometry = useMemo(() => createArrowHandleGeometry(), []) + + useEffect(() => { + return () => { + if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') { + document.body.style.cursor = '' + } + } + }, []) + + useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry]) + + const activateWallMove = (event: ThreeEvent) => { + event.stopPropagation() + event.nativeEvent.preventDefault() + document.body.style.cursor = 'grabbing' + + sfxEmitter.emit('sfx:item-pick') + useEditor.getState().setMovingNode(wall) + useEditor.getState().setMovingWallEndpoint(null) + useEditor.getState().setMovingFenceEndpoint(null) + useEditor.getState().setCurvingWall(null) + useEditor.getState().setCurvingFence(null) + useViewer.getState().setSelection({ selectedIds: [] }) + } + + return ( + + { + event.stopPropagation() + setIsHovered(true) + document.body.style.cursor = 'grab' + }} + onPointerLeave={(event) => { + event.stopPropagation() + setIsHovered(false) + if (document.body.style.cursor === 'grab') { + document.body.style.cursor = '' + } + }} + renderOrder={1002} + > + + + + + ) +} + +function getWallMoveHandles(wall: WallNode): WallMoveHandle[] { + const dx = wall.end[0] - wall.start[0] + const dz = wall.end[1] - wall.start[1] + const length = Math.hypot(dx, dz) + + if (length < 1e-6) { + return [] + } + + const normal: [number, number] = [-dz / length, dx / length] + const midpoint: [number, number] = [ + (wall.start[0] + wall.end[0]) / 2, + (wall.start[1] + wall.end[1]) / 2, + ] + const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT + const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT) + const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET) + + return [ + buildWallMoveHandle('front', midpoint, normal, offset, handleHeight), + buildWallMoveHandle('back', midpoint, [-normal[0], -normal[1]], offset, handleHeight), + ] +} + +function buildWallMoveHandle( + key: string, + midpoint: [number, number], + direction: [number, number], + offset: number, + height: number, +): WallMoveHandle { + return { + direction, + key, + position: [midpoint[0] + direction[0] * offset, height, midpoint[1] + direction[1] * offset], + rotationY: Math.atan2(-direction[1], direction[0]), + } +} diff --git a/packages/editor/src/components/feedback-dialog.tsx b/packages/editor/src/components/feedback-dialog.tsx deleted file mode 100644 index c38471c8..00000000 --- a/packages/editor/src/components/feedback-dialog.tsx +++ /dev/null @@ -1,265 +0,0 @@ -'use client' - -import { useScene } from '@pascal-app/core' -import { ImageIcon, MessageSquare, X } from 'lucide-react' -import { useCallback, useRef, useState } from 'react' -import { Button } from './ui/primitives/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from './ui/primitives/dialog' - -const MAX_IMAGES = 5 -const MAX_IMAGE_SIZE = 5 * 1024 * 1024 - -type ImagePreview = { file: File; url: string } - -export function FeedbackDialog({ - projectId: projectIdProp, - onSubmit, -}: { - projectId?: string - onSubmit?: (data: { - message: string - projectId?: string - sceneGraph: unknown - images: File[] - }) => Promise<{ success: boolean; error?: string }> -}) { - const projectId = projectIdProp - - const [open, setOpen] = useState(false) - const [message, setMessage] = useState('') - const [images, setImages] = useState([]) - const [isDragging, setIsDragging] = useState(false) - const [isSubmitting, setIsSubmitting] = useState(false) - const [error, setError] = useState(null) - const [sent, setSent] = useState(false) - const fileInputRef = useRef(null) - const dragCounter = useRef(0) - - const handleOpen = () => { - setOpen(true) - setSent(false) - setError(null) - setMessage('') - setImages([]) - setIsDragging(false) - dragCounter.current = 0 - } - - const handleClose = () => { - if (isSubmitting) return - setOpen(false) - images.forEach((img) => { - URL.revokeObjectURL(img.url) - }) - } - - const addFiles = useCallback((files: FileList | File[]) => { - const incoming = Array.from(files).filter( - (f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE, - ) - setImages((prev) => { - const remaining = MAX_IMAGES - prev.length - const added = incoming.slice(0, remaining).map((file) => ({ - file, - url: URL.createObjectURL(file), - })) - return [...prev, ...added] - }) - }, []) - - const removeImage = (index: number) => { - setImages((prev) => { - const img = prev[index] - if (img) URL.revokeObjectURL(img.url) - return prev.filter((_, i) => i !== index) - }) - } - - // ── Drag handlers (on the entire dialog content) ── - const onDragEnter = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - dragCounter.current++ - if (e.dataTransfer.types.includes('Files')) { - setIsDragging(true) - } - } - - const onDragLeave = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - dragCounter.current-- - if (dragCounter.current === 0) { - setIsDragging(false) - } - } - - const onDragOver = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - } - - const onDrop = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - dragCounter.current = 0 - setIsDragging(false) - if (e.dataTransfer.files.length > 0) { - addFiles(e.dataTransfer.files) - } - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError(null) - setIsSubmitting(true) - - try { - if (!onSubmit) return - const { nodes, rootNodeIds } = useScene.getState() - const sceneGraph = { nodes, rootNodeIds } - const result = await onSubmit({ - message, - projectId, - sceneGraph, - images: images.map((img) => img.file), - }) - if (result.success) { - setSent(true) - setTimeout(() => setOpen(false), 1500) - } else { - setError(result.error ?? 'Something went wrong') - } - } finally { - setIsSubmitting(false) - } - } - - return ( - <> - - - - - {/* Drag overlay — only visible when dragging files over the dialog */} - {isDragging && ( -
-
- -

Drop images here

-
-
- )} - - - Send Feedback - We'd love to hear your thoughts - - - {sent ? ( -

- Thanks for your feedback! -

- ) : ( -
-
- -