Merge remote-tracking branch 'origin/main' into feat/duplicate-project
This commit is contained in:
+6
-7
@@ -1,8 +1,7 @@
|
|||||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_google_maps_api_key
|
# Optional environment variables — the editor works without any of these
|
||||||
PORT=3000
|
|
||||||
POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:55322/postgres
|
|
||||||
|
|
||||||
# Run `bun db:start` then `bunx supabase status -o env` to get these values
|
# Google Maps API key (enables address search)
|
||||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:55321
|
# NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_google_maps_api_key
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
||||||
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
|
# Dev server port (default: 3000)
|
||||||
|
# PORT=3000
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: 🐛 Bug Report
|
||||||
|
description: Report something that isn't working correctly
|
||||||
|
labels: ["bug", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to report a bug! Please fill out the sections below so we can reproduce and fix the issue.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: A clear description of the bug.
|
||||||
|
placeholder: Describe what went wrong...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: How can we reproduce the issue?
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser & OS
|
||||||
|
description: e.g. Chrome 120, macOS 14
|
||||||
|
placeholder: Chrome 120, macOS 14
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots or screen recordings
|
||||||
|
description: If applicable, add screenshots or a short video to help explain the problem.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Any other information that might be helpful (console errors, related issues, etc.)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 💬 Questions & Help
|
||||||
|
url: https://github.com/pascalorg/editor/discussions/categories/q-a
|
||||||
|
about: Ask questions and get help from the community
|
||||||
|
- name: 💡 Ideas & Discussion
|
||||||
|
url: https://github.com/pascalorg/editor/discussions/categories/ideas
|
||||||
|
about: Share ideas and discuss features before opening a formal request
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: ✨ Feature Request
|
||||||
|
description: Suggest a new feature or improvement
|
||||||
|
labels: ["enhancement", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
We love hearing ideas from the community! Please describe what you'd like to see in Pascal Editor.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem or motivation
|
||||||
|
description: What problem does this solve? Why do you want this?
|
||||||
|
placeholder: I'm always frustrated when...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: How do you think this should work?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: Any workarounds or alternative approaches you've thought about?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Mockups, screenshots, references to other tools, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
## What does this PR do?
|
||||||
|
|
||||||
|
<!-- A brief description of the change. Link to a related issue if applicable: Fixes #123 -->
|
||||||
|
|
||||||
|
## How to test
|
||||||
|
|
||||||
|
<!-- Steps for reviewers to verify this change works -->
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Screenshots / recordings
|
||||||
|
|
||||||
|
<!-- If this is a visual change, add before/after screenshots or a short recording -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I've tested this locally with `bun dev`
|
||||||
|
- [ ] My code follows the existing code style (run `bun check` to verify)
|
||||||
|
- [ ] I've updated relevant documentation (if applicable)
|
||||||
|
- [ ] This PR targets the `main` branch
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Pascal Editor V2 — Architecture
|
# Pascal Editor — Architecture
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -10,8 +10,6 @@ apps/
|
|||||||
packages/
|
packages/
|
||||||
core/ # Scene schema, state, systems, spatial logic
|
core/ # Scene schema, state, systems, spatial logic
|
||||||
viewer/ # 3D canvas component (React Three Fiber)
|
viewer/ # 3D canvas component (React Three Fiber)
|
||||||
auth/ # Authentication (better-auth + Supabase)
|
|
||||||
db/ # Database layer (Drizzle ORM + Supabase)
|
|
||||||
ui/ # Shared React UI components
|
ui/ # Shared React UI components
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,6 +88,4 @@ User input (pointer/keyboard)
|
|||||||
| Framework | Next.js 16, React 19 |
|
| Framework | Next.js 16, React 19 |
|
||||||
| State | Zustand + Zundo |
|
| State | Zustand + Zundo |
|
||||||
| UI | Radix UI, Tailwind CSS 4 |
|
| UI | Radix UI, Tailwind CSS 4 |
|
||||||
| Database | Supabase PostgreSQL + Drizzle ORM |
|
|
||||||
| Auth | better-auth |
|
|
||||||
| Tooling | Biome, TypeScript 5.9, Turborepo |
|
| Tooling | Biome, TypeScript 5.9, Turborepo |
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Contributing to Pascal Editor
|
||||||
|
|
||||||
|
Thanks for your interest in contributing! We welcome all kinds of contributions — bug fixes, new features, documentation, and ideas.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Bun](https://bun.sh/) 1.3+ (or Node.js 18+)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/pascalorg/editor.git
|
||||||
|
cd editor
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The editor will be running at **http://localhost:3000**. That's it!
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and add a Google Maps API key if you want address search functionality. The editor works fully without it.
|
||||||
|
|
||||||
|
## Making changes
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
|
||||||
|
We use [Biome](https://biomejs.dev/) for linting and formatting. Before submitting a PR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun check # Check for issues
|
||||||
|
bun check:fix # Auto-fix issues
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project structure
|
||||||
|
|
||||||
|
| Package | What it does |
|
||||||
|
|---------|-------------|
|
||||||
|
| `packages/core` | Scene schema, state management, systems — no UI |
|
||||||
|
| `packages/viewer` | 3D rendering with React Three Fiber |
|
||||||
|
| `apps/editor` | The full editor app (Next.js) |
|
||||||
|
|
||||||
|
A key rule: **`packages/viewer` must never import from `apps/editor`**. The viewer is a standalone component; editor-specific behavior is injected via props/children.
|
||||||
|
|
||||||
|
## Submitting a PR
|
||||||
|
|
||||||
|
1. **Fork the repo** and create a branch from `main`
|
||||||
|
2. **Make your changes** and test locally with `bun dev`
|
||||||
|
3. **Run `bun check`** to make sure linting passes
|
||||||
|
4. **Open a PR** with a clear description of what changed and why
|
||||||
|
5. **Link related issues** if applicable (e.g., "Fixes #42")
|
||||||
|
|
||||||
|
### PR tips
|
||||||
|
|
||||||
|
- Keep PRs focused — one feature or fix per PR
|
||||||
|
- Include screenshots or recordings for visual changes
|
||||||
|
- If you're unsure about an approach, open an issue or discussion first
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
Use the [Bug Report](https://github.com/pascalorg/editor/issues/new?template=bug_report.yml) template. Include steps to reproduce — this helps us fix things faster.
|
||||||
|
|
||||||
|
## Suggesting features
|
||||||
|
|
||||||
|
Use the [Feature Request](https://github.com/pascalorg/editor/issues/new?template=feature_request.yml) template, or start a [Discussion](https://github.com/pascalorg/editor/discussions) if you want to brainstorm first.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Head to [Discussions](https://github.com/pascalorg/editor/discussions) — we're happy to help!
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
A 3D building editor built with React Three Fiber and WebGPU.
|
A 3D building editor built with React Three Fiber and WebGPU.
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://www.npmjs.com/package/@pascal-app/core)
|
||||||
|
[](https://www.npmjs.com/package/@pascal-app/viewer)
|
||||||
|
[](https://discord.gg/SaBRA9t2)
|
||||||
|
[](https://x.com/pascal_app)
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b
|
https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b
|
||||||
|
|
||||||
@@ -401,3 +405,14 @@ npm publish --workspace=@pascal-app/viewer --access public
|
|||||||
| `packages/viewer/src/components/viewer/` | Main Viewer component |
|
| `packages/viewer/src/components/viewer/` | Main Viewer component |
|
||||||
| `apps/editor/components/tools/` | Editor tools |
|
| `apps/editor/components/tools/` | Editor tools |
|
||||||
| `apps/editor/store/` | Editor-specific state |
|
| `apps/editor/store/` | Editor-specific state |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/Aymericr"><img src="https://avatars.githubusercontent.com/u/4444492?v=4" width="60" height="60" alt="Aymeric Rabot" style="border-radius:50%"></a>
|
||||||
|
<a href="https://github.com/wass08"><img src="https://avatars.githubusercontent.com/u/6551176?v=4" width="60" height="60" alt="Wassim Samad" style="border-radius:50%"></a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/23831" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23831" alt="pascalorg/editor | Trendshift" width="250" height="55"/></a>
|
||||||
|
|||||||
@@ -1,254 +1,55 @@
|
|||||||
# Pascal Editor - Setup Guide
|
# Pascal Editor — Setup
|
||||||
|
|
||||||
This guide will help you set up the Pascal Editor with authentication and database integration.
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Node.js 18+ or Bun 1.3+
|
- [Bun](https://bun.sh/) 1.3+ (or Node.js 18+)
|
||||||
- Docker Desktop (for running Supabase locally)
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Install Dependencies
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
```
|
|
||||||
|
|
||||||
This installs the Supabase CLI as a dev dependency - no need for global installation!
|
|
||||||
|
|
||||||
### 2. Start Supabase Local Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun db:start
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start a local Supabase instance. You'll see output like:
|
|
||||||
|
|
||||||
```
|
|
||||||
API URL: http://127.0.0.1:54321
|
|
||||||
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
||||||
Studio URL: http://127.0.0.1:54323
|
|
||||||
Anon key: eyJh...
|
|
||||||
Service role key: eyJh...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Configure Environment Variables
|
|
||||||
|
|
||||||
Create `apps/editor/.env.local` with the following variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Database Connection (Supabase local)
|
|
||||||
POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
||||||
|
|
||||||
# Supabase Configuration
|
|
||||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
|
||||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
|
||||||
|
|
||||||
# Better Auth (generate your own secret with: openssl rand -base64 32)
|
|
||||||
BETTER_AUTH_SECRET=<generate_with_command_below>
|
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Google Maps (optional, for address search)
|
|
||||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=<your_google_maps_key>
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate a secret for `BETTER_AUTH_SECRET`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 32
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Run Database Migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun db:reset
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create all necessary tables for authentication and properties.
|
|
||||||
|
|
||||||
### 6. Start the Development Server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The editor will be available at http://localhost:3000
|
The editor will be running at **http://localhost:3000**.
|
||||||
|
|
||||||
|
## Environment Variables (optional)
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` if you need:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` | No | Enables address search in the editor |
|
||||||
|
| `PORT` | No | Dev server port (default: 3000) |
|
||||||
|
|
||||||
|
The editor works fully without any environment variables.
|
||||||
|
|
||||||
## Monorepo Structure
|
## Monorepo Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
|
||||||
├── apps/
|
├── apps/
|
||||||
│ └── editor/ # Next.js editor application
|
│ └── editor/ # Next.js editor application
|
||||||
│ ├── app/
|
|
||||||
│ │ └── api/auth/ # Better Auth API routes
|
|
||||||
│ ├── components/ # UI components
|
|
||||||
│ └── features/
|
|
||||||
│ └── cloud-sync/ # Cloud sync feature
|
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── auth/ # @pascal-app/auth - Authentication package
|
│ ├── core/ # @pascal-app/core — Scene schema, state, systems
|
||||||
│ │ ├── src/
|
│ ├── viewer/ # @pascal-app/viewer — 3D rendering
|
||||||
│ │ │ ├── server.ts # Better Auth server config
|
│ └── ui/ # Shared UI components
|
||||||
│ │ │ └── client.ts # Better Auth client
|
└── tooling/ # Build & release tooling
|
||||||
│ │ └── README.md
|
|
||||||
│ ├── db/ # @pascal-app/db - Database package
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── client.ts # Supabase client (with RLS)
|
|
||||||
│ │ │ ├── server.ts # Supabase admin client
|
|
||||||
│ │ │ └── types.ts # Database types
|
|
||||||
│ │ ├── supabase/
|
|
||||||
│ │ │ ├── config.toml
|
|
||||||
│ │ │ └── migrations/ # SQL migrations
|
|
||||||
│ │ └── README.md
|
|
||||||
│ ├── core/ # @pascal-app/core - Core editor logic
|
|
||||||
│ ├── viewer/ # @pascal-app/viewer - 3D viewer
|
|
||||||
│ └── ui/ # @repo/ui - Shared UI components
|
|
||||||
└── turbo.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database Schema
|
## Scripts
|
||||||
|
|
||||||
### Auth Tables (Better Auth)
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `bun dev` | Start the development server |
|
||||||
|
| `bun build` | Build all packages |
|
||||||
|
| `bun check` | Lint and format check (Biome) |
|
||||||
|
| `bun check:fix` | Auto-fix lint and format issues |
|
||||||
|
| `bun check-types` | TypeScript type checking |
|
||||||
|
|
||||||
- **users** - User accounts with email and profile
|
## Contributing
|
||||||
- **sessions** - Active authentication sessions
|
|
||||||
- **accounts** - OAuth provider accounts (for future use)
|
|
||||||
- **verification_tokens** - Magic link tokens
|
|
||||||
|
|
||||||
### Application Tables
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on submitting PRs and reporting issues.
|
||||||
|
|
||||||
- **properties** - User properties
|
|
||||||
- `id`: Property ID
|
|
||||||
- `name`: Property name
|
|
||||||
- `owner_id`: User ID (foreign key to users)
|
|
||||||
|
|
||||||
- **properties_addresses** - Property addresses with Google Maps data
|
|
||||||
- `id`: Address ID
|
|
||||||
- `property_id`: Property ID (foreign key)
|
|
||||||
- `formatted_address`: Full address
|
|
||||||
- `latitude`, `longitude`: GPS coordinates
|
|
||||||
- Plus detailed address components (street, city, state, etc.)
|
|
||||||
|
|
||||||
- **properties_models** - Scene graph models (versions)
|
|
||||||
- `id`: Model ID
|
|
||||||
- `property_id`: Property ID (foreign key)
|
|
||||||
- `name`: Model name
|
|
||||||
- `version`: Version number
|
|
||||||
- `draft`: Draft status
|
|
||||||
- `scene_graph`: JSONB scene graph data
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
- **Magic Link Sign-In**: Passwordless authentication via email
|
|
||||||
- **Session Management**: 7-day sessions with automatic refresh
|
|
||||||
- **Cookie-based**: Secure httpOnly cookies
|
|
||||||
|
|
||||||
### Property Management
|
|
||||||
|
|
||||||
- **Create Properties**: Add properties with real-world addresses
|
|
||||||
- **Google Maps Integration**: Address autocomplete and geocoding
|
|
||||||
- **Switch Properties**: Seamlessly switch between properties
|
|
||||||
|
|
||||||
### Scene Management
|
|
||||||
|
|
||||||
- **Auto-Save**: Changes saved every 2 seconds
|
|
||||||
- **Scene Loading**: Automatic scene loading when switching properties
|
|
||||||
- **Version Control**: Models are versioned for future rollback support
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Making Database Changes
|
|
||||||
|
|
||||||
1. Create a new migration:
|
|
||||||
```bash
|
|
||||||
cd packages/db
|
|
||||||
supabase migration new your_migration_name
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Edit the migration file in `supabase/migrations/`
|
|
||||||
|
|
||||||
3. Apply the migration:
|
|
||||||
```bash
|
|
||||||
supabase db reset
|
|
||||||
```
|
|
||||||
|
|
||||||
### Updating Database Types
|
|
||||||
|
|
||||||
After changing the database schema, regenerate TypeScript types:
|
|
||||||
|
|
||||||
1. Update `packages/db/src/types.ts` to match your new schema
|
|
||||||
2. Run `bun install` to update type checking
|
|
||||||
|
|
||||||
### Testing Authentication
|
|
||||||
|
|
||||||
1. Start the editor: `bun dev`
|
|
||||||
2. Click "Save to cloud" button
|
|
||||||
3. Enter your email
|
|
||||||
4. Check console for magic link (not sent via email in development)
|
|
||||||
5. Click the link to authenticate
|
|
||||||
|
|
||||||
## Supabase Studio
|
|
||||||
|
|
||||||
Access the local Supabase Studio at: http://127.0.0.1:54323
|
|
||||||
|
|
||||||
Use this to:
|
|
||||||
- Browse and edit tables
|
|
||||||
- Run SQL queries
|
|
||||||
- View logs
|
|
||||||
- Manage RLS policies
|
|
||||||
- Test database functions
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
For production deployment:
|
|
||||||
|
|
||||||
1. Create a Supabase project at https://supabase.com
|
|
||||||
2. Get your production database connection string
|
|
||||||
3. Update environment variables in your hosting platform
|
|
||||||
4. Link and push migrations:
|
|
||||||
```bash
|
|
||||||
cd packages/db
|
|
||||||
bunx supabase link --project-ref your-project-ref
|
|
||||||
bunx supabase db push
|
|
||||||
```
|
|
||||||
5. Configure email provider in `packages/auth/src/server.ts`
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "Missing POSTGRES_URL" error
|
|
||||||
|
|
||||||
Make sure you've set `POSTGRES_URL` in `apps/editor/.env.local` to your Supabase connection string.
|
|
||||||
|
|
||||||
### Supabase not starting
|
|
||||||
|
|
||||||
Try stopping and restarting:
|
|
||||||
```bash
|
|
||||||
bun db:stop
|
|
||||||
bun db:start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migration errors
|
|
||||||
|
|
||||||
Reset the database:
|
|
||||||
```bash
|
|
||||||
cd packages/db
|
|
||||||
supabase db reset
|
|
||||||
```
|
|
||||||
|
|
||||||
### Auth not working
|
|
||||||
|
|
||||||
1. Check that Better Auth API route exists at `apps/editor/app/api/auth/[...all]/route.ts`
|
|
||||||
2. Verify `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are set
|
|
||||||
3. Check console for magic link URLs in development
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- Configure email provider for magic links
|
|
||||||
- Add OAuth providers (Google, GitHub, etc.)
|
|
||||||
- Set up production Supabase project
|
|
||||||
- Configure RLS policies for additional security
|
|
||||||
- Add more property features (sharing, collaboration, etc.)
|
|
||||||
|
|||||||
@@ -221,11 +221,11 @@
|
|||||||
|
|
||||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
"@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.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
@@ -233,23 +233,23 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="],
|
"@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/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="],
|
"@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-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="],
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="],
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="],
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="],
|
"@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-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="],
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="],
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.9", "", { "os": "win32", "cpu": "x64" }, "sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w=="],
|
||||||
|
|
||||||
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
|
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
|
|
||||||
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
|
"@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/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@
|
|||||||
|
|
||||||
"@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="],
|
"@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.12", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.14", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-ogBjgsFrPPz19abP3VwcYSahbkUOMMvJjxCOYWYndw+PydeMuLuB4XrvNkNutFrTjC9St2KFULRdKID8Sd/CMQ=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q=="],
|
||||||
|
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@react-grab/cli": ["@react-grab/cli@0.1.28", "", { "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-IE4bTeH0mCq0FBRaYRUtdiIfvO7NCv14lHS4TiTY8YNG5tPYwHnLZZtniud0ElmLIWGP95Kk6E7A6J2uDmOY+Q=="],
|
"@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-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/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=="],
|
||||||
|
|
||||||
@@ -563,35 +563,47 @@
|
|||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
"@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/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
"@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-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
"@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-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
"@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-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
"@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-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
"@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-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "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-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
"@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-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
|
||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
|
"@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=="],
|
||||||
|
|
||||||
|
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="],
|
||||||
|
|
||||||
|
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="],
|
||||||
|
|
||||||
|
"@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="],
|
||||||
|
|
||||||
|
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="],
|
||||||
|
|
||||||
|
"@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="],
|
||||||
|
|
||||||
|
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
|
||||||
|
|
||||||
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
||||||
|
|
||||||
@@ -619,25 +631,25 @@
|
|||||||
|
|
||||||
"@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
|
"@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
|
"@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/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
|
"@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/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
|
"@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/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
|
"@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/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
|
"@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/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "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-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="],
|
"@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/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "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-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
|
"@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/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
|
"@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/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
"@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=="],
|
||||||
|
|
||||||
"@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
|
"@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
|
||||||
|
|
||||||
@@ -689,7 +701,7 @@
|
|||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
|
||||||
|
|
||||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||||
|
|
||||||
@@ -713,7 +725,7 @@
|
|||||||
|
|
||||||
"camera-controls": ["camera-controls@3.1.2", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA=="],
|
"camera-controls": ["camera-controls@3.1.2", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
|
||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
@@ -787,11 +799,13 @@
|
|||||||
|
|
||||||
"editor": ["editor@workspace:apps/editor"],
|
"editor": ["editor@workspace:apps/editor"],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="],
|
"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=="],
|
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
|
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||||
|
|
||||||
"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.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=="],
|
||||||
|
|
||||||
@@ -825,7 +839,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-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.16", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-fhvTLe0PnyV72npfBi/IwwVMzkTnUw9MxS2S2Q0Oo1aed7woAtHwSL5DEdTTV6tXZSKbqRHaIbjV6rwx+1iRYA=="],
|
"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-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
@@ -867,11 +881,11 @@
|
|||||||
|
|
||||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
"flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="],
|
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||||
|
|
||||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||||
|
|
||||||
"framer-motion": ["framer-motion@12.36.0", "", { "dependencies": { "motion-dom": "^12.36.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw=="],
|
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
@@ -1033,29 +1047,29 @@
|
|||||||
|
|
||||||
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
@@ -1065,7 +1079,7 @@
|
|||||||
|
|
||||||
"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=="],
|
"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.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
|
||||||
|
|
||||||
"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@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=="],
|
||||||
|
|
||||||
@@ -1093,15 +1107,15 @@
|
|||||||
|
|
||||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
"motion": ["motion@12.36.0", "", { "dependencies": { "framer-motion": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5BMQuktYUX8aEByKWYx5tR4X3G08H2OMgp46wTxZ4o7CDDstyy4A0fe9RLNMjZiwvntCWGDvs16sC87/emz4Yw=="],
|
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.36.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA=="],
|
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
||||||
|
|
||||||
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
|
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
"nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
|
||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
@@ -1155,7 +1169,7 @@
|
|||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
"polygon-clipping": ["polygon-clipping@0.15.7", "", { "dependencies": { "robust-predicates": "^3.0.2", "splaytree": "^3.1.0" } }, "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA=="],
|
"polygon-clipping": ["polygon-clipping@0.15.7", "", { "dependencies": { "robust-predicates": "^3.0.2", "splaytree": "^3.1.0" } }, "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA=="],
|
||||||
|
|
||||||
@@ -1185,7 +1199,7 @@
|
|||||||
|
|
||||||
"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.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
"react-grab": ["react-grab@0.1.28", "", { "dependencies": { "@medv/finder": "^4.0.2", "@react-grab/cli": "0.1.28", "bippy": "^0.5.32", "solid-js": "^1.9.10" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-u3fvu7a7ejHhuWKzf/N6sFavV04vcqwtbcqyxwNPtvd3ts9KSVerpHp1ZH0/XVKTsh3MZz1u1jyIt0KYC9L/Rg=="],
|
"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-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
@@ -1213,7 +1227,7 @@
|
|||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
@@ -1255,9 +1269,9 @@
|
|||||||
|
|
||||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||||
|
|
||||||
"smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
|
"smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
||||||
|
|
||||||
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
|
"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=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
@@ -1297,9 +1311,9 @@
|
|||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
||||||
|
|
||||||
"three": ["three@0.183.2", "", {}, "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ=="],
|
"three": ["three@0.183.2", "", {}, "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ=="],
|
||||||
|
|
||||||
@@ -1309,7 +1323,7 @@
|
|||||||
|
|
||||||
"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=="],
|
"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.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
@@ -1321,25 +1335,13 @@
|
|||||||
|
|
||||||
"troika-worker-utils": ["troika-worker-utils@0.52.0", "", {}, "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="],
|
"troika-worker-utils": ["troika-worker-utils@0.52.0", "", {}, "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
|
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.8.16", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.16", "turbo-darwin-arm64": "2.8.16", "turbo-linux-64": "2.8.16", "turbo-linux-arm64": "2.8.16", "turbo-windows-64": "2.8.16", "turbo-windows-arm64": "2.8.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA=="],
|
"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-darwin-64": ["turbo-darwin-64@2.8.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg=="],
|
|
||||||
|
|
||||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow=="],
|
|
||||||
|
|
||||||
"turbo-linux-64": ["turbo-linux-64@2.8.16", "", { "os": "linux", "cpu": "x64" }, "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w=="],
|
|
||||||
|
|
||||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ=="],
|
|
||||||
|
|
||||||
"turbo-windows-64": ["turbo-windows-64@2.8.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw=="],
|
|
||||||
|
|
||||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw=="],
|
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
@@ -1355,9 +1357,9 @@
|
|||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="],
|
"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=="],
|
||||||
|
|
||||||
"ultracite": ["ultracite@7.3.0", "", { "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-lnzknmRmaLNPk2cYCpT+dI78Jyz+0Or1o8XOjHNxeIrBGrbQXZOTNwQjcFeYaQ1Ov5HK1NqPQirmzyMgA4lJSw=="],
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
@@ -1405,7 +1407,7 @@
|
|||||||
|
|
||||||
"zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="],
|
"zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="],
|
||||||
|
|
||||||
"zustand": ["zustand@5.0.11", "", { "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-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
|
"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=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
@@ -1443,9 +1445,9 @@
|
|||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="],
|
"@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=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
|
"@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/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||||
|
|
||||||
@@ -1481,7 +1483,7 @@
|
|||||||
|
|
||||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
@@ -1499,11 +1501,11 @@
|
|||||||
|
|
||||||
"@repo/ui/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"@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.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
"editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pascal-app/core",
|
"name": "@pascal-app/core",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "Core library for Pascal 3D building editor",
|
"description": "Core library for Pascal 3D building editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import type { SceneState } from '../use-scene'
|
|||||||
|
|
||||||
type AnyContainerNode = AnyNode & { children: string[] }
|
type AnyContainerNode = AnyNode & { children: string[] }
|
||||||
|
|
||||||
|
// Track pending RAF for updateNodesAction to prevent multiple queued callbacks
|
||||||
|
let pendingRafId: number | null = null
|
||||||
|
let pendingUpdates: Set<AnyNodeId> = new Set()
|
||||||
|
|
||||||
export const createNodesAction = (
|
export const createNodesAction = (
|
||||||
set: (fn: (state: SceneState) => Partial<SceneState>) => void,
|
set: (fn: (state: SceneState) => Partial<SceneState>) => void,
|
||||||
get: () => SceneState,
|
get: () => SceneState,
|
||||||
@@ -58,6 +62,7 @@ export const updateNodesAction = (
|
|||||||
updates: { id: AnyNodeId; data: Partial<AnyNode> }[],
|
updates: { id: AnyNodeId; data: Partial<AnyNode> }[],
|
||||||
) => {
|
) => {
|
||||||
const parentsToUpdate = new Set<AnyNodeId>()
|
const parentsToUpdate = new Set<AnyNodeId>()
|
||||||
|
const idsToMarkDirty = new Set<AnyNodeId>()
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextNodes = { ...state.nodes }
|
const nextNodes = { ...state.nodes }
|
||||||
@@ -98,14 +103,25 @@ export const updateNodesAction = (
|
|||||||
return { nodes: nextNodes }
|
return { nodes: nextNodes }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mark dirty after the next frame to ensure React renders complete
|
// Collect all IDs that need to be marked dirty
|
||||||
requestAnimationFrame(() => {
|
updates.forEach((u) => idsToMarkDirty.add(u.id))
|
||||||
updates.forEach((u) => {
|
parentsToUpdate.forEach((pId) => idsToMarkDirty.add(pId))
|
||||||
get().markDirty(u.id)
|
|
||||||
})
|
// Add to pending updates set
|
||||||
parentsToUpdate.forEach((pId) => {
|
idsToMarkDirty.forEach((id) => pendingUpdates.add(id))
|
||||||
get().markDirty(pId)
|
|
||||||
|
// Cancel any pending RAF and schedule a new one
|
||||||
|
if (pendingRafId !== null) {
|
||||||
|
cancelAnimationFrame(pendingRafId)
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRafId = requestAnimationFrame(() => {
|
||||||
|
// Mark all pending updates as dirty
|
||||||
|
pendingUpdates.forEach((id) => {
|
||||||
|
get().markDirty(id)
|
||||||
})
|
})
|
||||||
|
pendingUpdates.clear()
|
||||||
|
pendingRafId = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +137,26 @@ export const deleteNodesAction = (
|
|||||||
const nextCollections = { ...state.collections }
|
const nextCollections = { ...state.collections }
|
||||||
let nextRootIds = [...state.rootNodeIds]
|
let nextRootIds = [...state.rootNodeIds]
|
||||||
|
|
||||||
|
// Collect all IDs to delete (including descendants) in a first pass
|
||||||
|
// This avoids issues with recursive calls during state mutation
|
||||||
|
const allIdsToDelete = new Set<AnyNodeId>()
|
||||||
|
const collectDescendants = (id: AnyNodeId) => {
|
||||||
|
const node = nextNodes[id]
|
||||||
|
if (!node) return
|
||||||
|
allIdsToDelete.add(id)
|
||||||
|
if ('children' in node && node.children) {
|
||||||
|
for (const childId of node.children as AnyNodeId[]) {
|
||||||
|
collectDescendants(childId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
|
collectDescendants(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process all nodes for deletion
|
||||||
|
for (const id of allIdsToDelete) {
|
||||||
const node = nextNodes[id]
|
const node = nextNodes[id]
|
||||||
if (!node) continue
|
if (!node) continue
|
||||||
|
|
||||||
@@ -153,12 +188,6 @@ export const deleteNodesAction = (
|
|||||||
|
|
||||||
// 4. Delete the node itself
|
// 4. Delete the node itself
|
||||||
delete nextNodes[id]
|
delete nextNodes[id]
|
||||||
|
|
||||||
// Inside the deleteNodes loop
|
|
||||||
if ('children' in node && node.children.length > 0) {
|
|
||||||
// Recursively delete all children first
|
|
||||||
get().deleteNodes(node.children as AnyNodeId[])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nodes: nextNodes, rootNodeIds: nextRootIds, collections: nextCollections }
|
return { nodes: nextNodes, rootNodeIds: nextRootIds, collections: nextCollections }
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ const useScene: UseSceneStore = create<SceneState>()(
|
|||||||
collections: {} as Record<CollectionId, Collection>,
|
collections: {} as Record<CollectionId, Collection>,
|
||||||
|
|
||||||
unloadScene: () => {
|
unloadScene: () => {
|
||||||
|
// Clear temporal tracking to prevent memory leaks from stale node references
|
||||||
|
prevPastLength = 0
|
||||||
|
prevFutureLength = 0
|
||||||
|
prevNodesSnapshot = null
|
||||||
|
|
||||||
set({
|
set({
|
||||||
nodes: {},
|
nodes: {},
|
||||||
rootNodeIds: [],
|
rootNodeIds: [],
|
||||||
@@ -306,13 +311,21 @@ let prevPastLength = 0
|
|||||||
let prevFutureLength = 0
|
let prevFutureLength = 0
|
||||||
let prevNodesSnapshot: Record<AnyNodeId, AnyNode> | null = null
|
let prevNodesSnapshot: Record<AnyNodeId, AnyNode> | null = null
|
||||||
|
|
||||||
export function clearSceneHistory() {
|
/**
|
||||||
useScene.temporal.getState().clear()
|
* Clears temporal history tracking variables to prevent memory leaks.
|
||||||
|
* Should be called when unloading a scene to release node references.
|
||||||
|
*/
|
||||||
|
export function clearTemporalTracking() {
|
||||||
prevPastLength = 0
|
prevPastLength = 0
|
||||||
prevFutureLength = 0
|
prevFutureLength = 0
|
||||||
prevNodesSnapshot = null
|
prevNodesSnapshot = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearSceneHistory() {
|
||||||
|
useScene.temporal.getState().clear()
|
||||||
|
clearTemporalTracking()
|
||||||
|
}
|
||||||
|
|
||||||
// Subscribe to the temporal store (Undo/Redo events)
|
// Subscribe to the temporal store (Undo/Redo events)
|
||||||
useScene.temporal.subscribe((state) => {
|
useScene.temporal.subscribe((state) => {
|
||||||
const currentPastLength = state.pastStates.length
|
const currentPastLength = state.pastStates.length
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const csgEvaluator = new Evaluator()
|
|||||||
// WALL SYSTEM
|
// WALL SYSTEM
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
let useFrameNb = 0
|
|
||||||
export const WallSystem = () => {
|
export const WallSystem = () => {
|
||||||
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
||||||
const clearDirty = useScene((state) => state.clearDirty)
|
const clearDirty = useScene((state) => state.clearDirty)
|
||||||
@@ -35,7 +34,6 @@ export const WallSystem = () => {
|
|||||||
// Collect dirty walls and their levels
|
// Collect dirty walls and their levels
|
||||||
const dirtyWallsByLevel = new Map<string, Set<string>>()
|
const dirtyWallsByLevel = new Map<string, Set<string>>()
|
||||||
|
|
||||||
useFrameNb += 1
|
|
||||||
dirtyNodes.forEach((id) => {
|
dirtyNodes.forEach((id) => {
|
||||||
const node = nodes[id]
|
const node = nodes[id]
|
||||||
if (!node || node.type !== 'wall') return
|
if (!node || node.type !== 'wall') return
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useThree } from '@react-three/fiber'
|
import { useThree } from '@react-three/fiber'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
||||||
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
||||||
|
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
||||||
|
|
||||||
export function ExportManager() {
|
export function ExportManager() {
|
||||||
const scene = useThree((state) => state.scene)
|
const scene = useThree((state) => state.scene)
|
||||||
const setExportScene = useViewer((state) => state.setExportScene)
|
const setExportScene = useViewer((state) => state.setExportScene)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const exportFn = async () => {
|
const exportFn = async (format: 'glb' | 'stl' | 'obj' = 'glb') => {
|
||||||
// Find the scene renderer group by name
|
// Find the scene renderer group by name
|
||||||
const sceneGroup = scene.getObjectByName('scene-renderer')
|
const sceneGroup = scene.getObjectByName('scene-renderer')
|
||||||
if (!sceneGroup) {
|
if (!sceneGroup) {
|
||||||
@@ -18,20 +20,33 @@ export function ExportManager() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const exporter = new GLTFExporter()
|
|
||||||
const date = new Date().toISOString().split('T')[0]
|
const date = new Date().toISOString().split('T')[0]
|
||||||
|
|
||||||
|
if (format === 'stl') {
|
||||||
|
const exporter = new STLExporter()
|
||||||
|
const result = exporter.parse(sceneGroup, { binary: true })
|
||||||
|
const blob = new Blob([result], { type: 'model/stl' })
|
||||||
|
downloadBlob(blob, `model_${date}.stl`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'obj') {
|
||||||
|
const exporter = new OBJExporter()
|
||||||
|
const result = exporter.parse(sceneGroup)
|
||||||
|
const blob = new Blob([result], { type: 'model/obj' })
|
||||||
|
downloadBlob(blob, `model_${date}.obj`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: GLB export (existing behavior)
|
||||||
|
const exporter = new GLTFExporter()
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
exporter.parse(
|
exporter.parse(
|
||||||
sceneGroup,
|
sceneGroup,
|
||||||
(gltf) => {
|
(gltf) => {
|
||||||
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
|
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
|
||||||
const url = URL.createObjectURL(blob)
|
downloadBlob(blob, `model_${date}.glb`)
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `model_${date}.glb`
|
|
||||||
link.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
resolve()
|
resolve()
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -52,3 +67,12 @@ export function ExportManager() {
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadBlob(blob: Blob, filename: string) {
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ import { sfxEmitter } from '../../lib/sfx-bus'
|
|||||||
import useEditor from '../../store/use-editor'
|
import useEditor from '../../store/use-editor'
|
||||||
import { NodeActionMenu } from './node-action-menu'
|
import { NodeActionMenu } from './node-action-menu'
|
||||||
|
|
||||||
const ALLOWED_TYPES = ['item', 'door', 'window', 'roof', 'roof-segment']
|
const ALLOWED_TYPES = ['item', 'door', 'window', 'roof', 'roof-segment', 'wall', 'slab']
|
||||||
|
const DELETE_ONLY_TYPES = ['wall', 'slab']
|
||||||
|
|
||||||
export function FloatingActionMenu() {
|
export function FloatingActionMenu() {
|
||||||
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
||||||
const nodes = useScene((s) => s.nodes)
|
const nodes = useScene((s) => s.nodes)
|
||||||
const deleteNode = useScene((s) => s.deleteNode)
|
const mode = useEditor((s) => s.mode)
|
||||||
|
const setMode = useEditor((s) => s.setMode)
|
||||||
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
||||||
const setMovingNode = useEditor((s) => s.setMovingNode)
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
||||||
const setSelection = useViewer((s) => s.setSelection)
|
const setSelection = useViewer((s) => s.setSelection)
|
||||||
@@ -46,8 +48,10 @@ export function FloatingActionMenu() {
|
|||||||
const box = new THREE.Box3().setFromObject(obj)
|
const box = new THREE.Box3().setFromObject(obj)
|
||||||
if (!box.isEmpty()) {
|
if (!box.isEmpty()) {
|
||||||
const center = box.getCenter(new THREE.Vector3())
|
const center = box.getCenter(new THREE.Vector3())
|
||||||
// Position slightly above the object
|
// Position above the object, with extra offset for walls/slabs to avoid covering measurement labels
|
||||||
groupRef.current.position.set(center.x, box.max.y + 0.3, center.z)
|
const isDeleteOnly = node && DELETE_ONLY_TYPES.includes(node.type)
|
||||||
|
const yOffset = isDeleteOnly ? 0.8 : 0.3
|
||||||
|
groupRef.current.position.set(center.x, box.max.y + yOffset, center.z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -151,16 +155,14 @@ export function FloatingActionMenu() {
|
|||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (!(selectedId && node)) return
|
// Activate delete mode (sledgehammer tool) instead of deleting directly
|
||||||
sfxEmitter.emit('sfx:item-delete')
|
|
||||||
deleteNode(selectedId as AnyNodeId)
|
|
||||||
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
|
||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
|
setMode('delete')
|
||||||
},
|
},
|
||||||
[selectedId, node, deleteNode, setSelection],
|
[setSelection, setMode],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!(selectedId && node && isValidType && !isFloorplanHovered)) return null
|
if (!(selectedId && node && isValidType && !isFloorplanHovered && mode !== 'delete')) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
@@ -174,8 +176,8 @@ export function FloatingActionMenu() {
|
|||||||
>
|
>
|
||||||
<NodeActionMenu
|
<NodeActionMenu
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onDuplicate={handleDuplicate}
|
onDuplicate={node && !DELETE_ONLY_TYPES.includes(node.type) ? handleDuplicate : undefined}
|
||||||
onMove={handleMove}
|
onMove={node && !DELETE_ONLY_TYPES.includes(node.type) ? handleMove : undefined}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onPointerUp={(e) => e.stopPropagation()}
|
onPointerUp={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|||||||
|
|
||||||
type NodeActionMenuProps = {
|
type NodeActionMenuProps = {
|
||||||
onDelete: MouseEventHandler<HTMLButtonElement>
|
onDelete: MouseEventHandler<HTMLButtonElement>
|
||||||
onDuplicate: MouseEventHandler<HTMLButtonElement>
|
onDuplicate?: MouseEventHandler<HTMLButtonElement>
|
||||||
onMove: MouseEventHandler<HTMLButtonElement>
|
onMove?: MouseEventHandler<HTMLButtonElement>
|
||||||
onPointerDown?: PointerEventHandler<HTMLDivElement>
|
onPointerDown?: PointerEventHandler<HTMLDivElement>
|
||||||
onPointerUp?: PointerEventHandler<HTMLDivElement>
|
onPointerUp?: PointerEventHandler<HTMLDivElement>
|
||||||
onPointerEnter?: PointerEventHandler<HTMLDivElement>
|
onPointerEnter?: PointerEventHandler<HTMLDivElement>
|
||||||
@@ -30,6 +30,7 @@ export function NodeActionMenu({
|
|||||||
onPointerLeave={onPointerLeave}
|
onPointerLeave={onPointerLeave}
|
||||||
onPointerUp={onPointerUp}
|
onPointerUp={onPointerUp}
|
||||||
>
|
>
|
||||||
|
{onMove && (
|
||||||
<button
|
<button
|
||||||
aria-label="Move"
|
aria-label="Move"
|
||||||
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||||
@@ -39,6 +40,8 @@ export function NodeActionMenu({
|
|||||||
>
|
>
|
||||||
<Move className="h-4 w-4" />
|
<Move className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{onDuplicate && (
|
||||||
<button
|
<button
|
||||||
aria-label="Duplicate"
|
aria-label="Duplicate"
|
||||||
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||||
@@ -48,6 +51,7 @@ export function NodeActionMenu({
|
|||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
aria-label="Delete"
|
aria-label="Delete"
|
||||||
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { useThree } from '@react-three/fiber'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
||||||
import useEditor, { type Phase, type StructureLayer } from './../../store/use-editor'
|
import useEditor, { type Phase, type StructureLayer } from './../../store/use-editor'
|
||||||
|
|
||||||
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
||||||
@@ -264,6 +266,79 @@ export const SelectionManager = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Delete mode: click-to-delete (sledgehammer tool)
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode !== 'delete') return
|
||||||
|
|
||||||
|
const onClick = (event: NodeEvent) => {
|
||||||
|
const node = event.node
|
||||||
|
if (!isNodeInCurrentLevel(node)) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
// Play appropriate SFX
|
||||||
|
if (node.type === 'item') {
|
||||||
|
sfxEmitter.emit('sfx:item-delete')
|
||||||
|
} else {
|
||||||
|
sfxEmitter.emit('sfx:structure-delete')
|
||||||
|
}
|
||||||
|
|
||||||
|
useScene.getState().deleteNode(node.id as AnyNodeId)
|
||||||
|
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
||||||
|
|
||||||
|
// Clear hover since the node is gone
|
||||||
|
if (useViewer.getState().hoveredId === node.id) {
|
||||||
|
useViewer.setState({ hoveredId: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEnter = (event: NodeEvent) => {
|
||||||
|
const node = event.node
|
||||||
|
if (!isNodeInCurrentLevel(node)) return
|
||||||
|
if (node.type === 'building' || node.type === 'site') return
|
||||||
|
event.stopPropagation()
|
||||||
|
useViewer.setState({ hoveredId: node.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLeave = (event: NodeEvent) => {
|
||||||
|
const nodeId = event?.node?.id
|
||||||
|
if (nodeId && useViewer.getState().hoveredId === nodeId) {
|
||||||
|
useViewer.setState({ hoveredId: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGridClick = () => {
|
||||||
|
// Clicking empty space in delete mode does nothing (stay in delete mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTypes = [
|
||||||
|
'wall',
|
||||||
|
'item',
|
||||||
|
'slab',
|
||||||
|
'ceiling',
|
||||||
|
'roof',
|
||||||
|
'roof-segment',
|
||||||
|
'window',
|
||||||
|
'door',
|
||||||
|
'zone',
|
||||||
|
]
|
||||||
|
allTypes.forEach((type) => {
|
||||||
|
emitter.on(`${type}:click` as any, onClick as any)
|
||||||
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||||
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||||
|
})
|
||||||
|
emitter.on('grid:click', onGridClick)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
allTypes.forEach((type) => {
|
||||||
|
emitter.off(`${type}:click` as any, onClick as any)
|
||||||
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||||
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||||
|
})
|
||||||
|
emitter.off('grid:click', onGridClick)
|
||||||
|
}
|
||||||
|
}, [mode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== 'select') return
|
if (mode !== 'select') return
|
||||||
if (movingNode) return
|
if (movingNode) return
|
||||||
@@ -475,12 +550,30 @@ export const SelectionManager = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DeleteModeCursor />
|
||||||
<SelectionStateSync />
|
<SelectionStateSync />
|
||||||
<EditorOutlinerSync />
|
<EditorOutlinerSync />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DeleteModeCursor = () => {
|
||||||
|
const mode = useEditor((s) => s.mode)
|
||||||
|
const gl = useThree((s) => s.gl)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = gl.domElement
|
||||||
|
if (mode === 'delete') {
|
||||||
|
canvas.style.cursor = 'crosshair'
|
||||||
|
return () => {
|
||||||
|
canvas.style.cursor = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mode, gl])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const SelectionStateSync = () => {
|
const SelectionStateSync = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return useScene.subscribe((state) => {
|
return useScene.subscribe((state) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
||||||
import { mix, positionLocal } from 'three/tsl'
|
import { mix, positionLocal } from 'three/tsl'
|
||||||
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { EDITOR_LAYER } from '../../../lib/constants'
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
@@ -183,6 +184,7 @@ export const CeilingTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
if (points.length > 0) markToolCancelConsumed()
|
||||||
setPoints([])
|
setPoints([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { BufferGeometry, DoubleSide, type Group, type Line, Vector3 } from 'three'
|
import { BufferGeometry, DoubleSide, type Group, type Line, Vector3 } from 'three'
|
||||||
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { EDITOR_LAYER } from '../../../lib/constants'
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
@@ -232,6 +233,7 @@ export const RoofTool: React.FC = () => {
|
|||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
if (corner1Ref.current) {
|
if (corner1Ref.current) {
|
||||||
|
markToolCancelConsumed()
|
||||||
corner1Ref.current = null
|
corner1Ref.current = null
|
||||||
outlineRef.current.visible = false
|
outlineRef.current.visible = false
|
||||||
setPreview((prev) => ({ ...prev, corner1: null }))
|
setPreview((prev) => ({ ...prev, corner1: null }))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { emitter, type GridEvent, type LevelNode, SlabNode, useScene } from '@pa
|
|||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
||||||
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { EDITOR_LAYER } from '../../../lib/constants'
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
@@ -150,6 +151,7 @@ export const SlabTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
if (points.length > 0) markToolCancelConsumed()
|
||||||
setPoints([])
|
setPoints([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,32 @@
|
|||||||
import { useScene, type WallNode, WallNode as WallSchema } from '@pascal-app/core'
|
import { useScene, type WallNode, WallNode as WallSchema } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
|
|
||||||
export type WallPlanPoint = [number, number]
|
export type WallPlanPoint = [number, number]
|
||||||
|
|
||||||
export const WALL_GRID_STEP = 0.5
|
export const WALL_GRID_STEP = 0.5
|
||||||
export const WALL_JOIN_SNAP_RADIUS = 0.35
|
export const WALL_JOIN_SNAP_RADIUS = 0.35
|
||||||
export const WALL_MIN_LENGTH = 0.01
|
export const WALL_MIN_LENGTH = 0.5
|
||||||
|
|
||||||
function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
|
function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
|
||||||
const dx = a[0] - b[0]
|
const dx = a[0] - b[0]
|
||||||
const dz = a[1] - b[1]
|
const dz = a[1] - b[1]
|
||||||
return dx * dx + dz * dz
|
return dx * dx + dz * dz
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
|
function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
|
||||||
return Math.round(value / step) * step
|
return Math.round(value / step) * step
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): WallPlanPoint {
|
export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): WallPlanPoint {
|
||||||
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
|
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snapPointTo45Degrees(start: WallPlanPoint, cursor: WallPlanPoint): WallPlanPoint {
|
export function snapPointTo45Degrees(start: WallPlanPoint, cursor: WallPlanPoint): WallPlanPoint {
|
||||||
const dx = cursor[0] - start[0]
|
const dx = cursor[0] - start[0]
|
||||||
const dz = cursor[1] - start[1]
|
const dz = cursor[1] - start[1]
|
||||||
const angle = Math.atan2(dz, dx)
|
const angle = Math.atan2(dz, dx)
|
||||||
const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4)
|
const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4)
|
||||||
const distance = Math.sqrt(dx * dx + dz * dz)
|
const distance = Math.sqrt(dx * dx + dz * dz)
|
||||||
|
|
||||||
return snapPointToGrid([
|
return snapPointToGrid([
|
||||||
start[0] + Math.cos(snappedAngle) * distance,
|
start[0] + Math.cos(snappedAngle) * distance,
|
||||||
start[1] + Math.sin(snappedAngle) * distance,
|
start[1] + Math.sin(snappedAngle) * distance,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
|
function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
|
||||||
const [x1, z1] = wall.start
|
const [x1, z1] = wall.start
|
||||||
const [x2, z2] = wall.end
|
const [x2, z2] = wall.end
|
||||||
@@ -44,15 +36,12 @@ function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoi
|
|||||||
if (lengthSquared < 1e-9) {
|
if (lengthSquared < 1e-9) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
|
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
|
||||||
if (t <= 0 || t >= 1) {
|
if (t <= 0 || t >= 1) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return [x1 + dx * t, z1 + dz * t]
|
return [x1 + dx * t, z1 + dz * t]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findWallSnapTarget(
|
export function findWallSnapTarget(
|
||||||
point: WallPlanPoint,
|
point: WallPlanPoint,
|
||||||
walls: WallNode[],
|
walls: WallNode[],
|
||||||
@@ -62,12 +51,10 @@ export function findWallSnapTarget(
|
|||||||
const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
|
const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
|
||||||
let bestTarget: WallPlanPoint | null = null
|
let bestTarget: WallPlanPoint | null = null
|
||||||
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
||||||
|
|
||||||
for (const wall of walls) {
|
for (const wall of walls) {
|
||||||
if (ignoreWallIds.has(wall.id)) {
|
if (ignoreWallIds.has(wall.id)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidates: Array<WallPlanPoint | null> = [
|
const candidates: Array<WallPlanPoint | null> = [
|
||||||
wall.start,
|
wall.start,
|
||||||
wall.end,
|
wall.end,
|
||||||
@@ -77,7 +64,6 @@ export function findWallSnapTarget(
|
|||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidateDistanceSquared = distanceSquared(point, candidate)
|
const candidateDistanceSquared = distanceSquared(point, candidate)
|
||||||
if (
|
if (
|
||||||
candidateDistanceSquared > radiusSquared ||
|
candidateDistanceSquared > radiusSquared ||
|
||||||
@@ -85,15 +71,12 @@ export function findWallSnapTarget(
|
|||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
bestTarget = candidate
|
bestTarget = candidate
|
||||||
bestDistanceSquared = candidateDistanceSquared
|
bestDistanceSquared = candidateDistanceSquared
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestTarget
|
return bestTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snapWallDraftPoint(args: {
|
export function snapWallDraftPoint(args: {
|
||||||
point: WallPlanPoint
|
point: WallPlanPoint
|
||||||
walls: WallNode[]
|
walls: WallNode[]
|
||||||
@@ -103,38 +86,31 @@ export function snapWallDraftPoint(args: {
|
|||||||
}): WallPlanPoint {
|
}): WallPlanPoint {
|
||||||
const { point, walls, start, angleSnap = false, ignoreWallIds } = args
|
const { point, walls, start, angleSnap = false, ignoreWallIds } = args
|
||||||
const basePoint = start && angleSnap ? snapPointTo45Degrees(start, point) : snapPointToGrid(point)
|
const basePoint = start && angleSnap ? snapPointTo45Degrees(start, point) : snapPointToGrid(point)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
findWallSnapTarget(basePoint, walls, {
|
findWallSnapTarget(basePoint, walls, {
|
||||||
ignoreWallIds,
|
ignoreWallIds,
|
||||||
}) ?? basePoint
|
}) ?? basePoint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWallLongEnough(start: WallPlanPoint, end: WallPlanPoint): boolean {
|
export function isWallLongEnough(start: WallPlanPoint, end: WallPlanPoint): boolean {
|
||||||
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWallOnCurrentLevel(
|
export function createWallOnCurrentLevel(
|
||||||
start: WallPlanPoint,
|
start: WallPlanPoint,
|
||||||
end: WallPlanPoint,
|
end: WallPlanPoint,
|
||||||
): WallNode | null {
|
): WallNode | null {
|
||||||
const currentLevelId = useViewer.getState().selection.levelId
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
const { createNode, nodes } = useScene.getState()
|
const { createNode, nodes } = useScene.getState()
|
||||||
|
|
||||||
if (!(currentLevelId && isWallLongEnough(start, end))) {
|
if (!(currentLevelId && isWallLongEnough(start, end))) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
|
const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
|
||||||
const wall = WallSchema.parse({
|
const wall = WallSchema.parse({
|
||||||
name: `Wall ${wallCount + 1}`,
|
name: `Wall ${wallCount + 1}`,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
createNode(wall, currentLevelId)
|
createNode(wall, currentLevelId)
|
||||||
sfxEmitter.emit('sfx:structure-build')
|
sfxEmitter.emit('sfx:structure-build')
|
||||||
|
|
||||||
return wall
|
return wall
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { emitter, type GridEvent, type LevelNode, useScene, type WallNode } from
|
|||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { DoubleSide, type Group, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three'
|
import { DoubleSide, type Group, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three'
|
||||||
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { EDITOR_LAYER } from '../../../lib/constants'
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
import { createWallOnCurrentLevel, snapWallDraftPoint, type WallPlanPoint } from './wall-drafting'
|
import { createWallOnCurrentLevel, snapWallDraftPoint, WALL_MIN_LENGTH, type WallPlanPoint } from './wall-drafting'
|
||||||
|
|
||||||
const WALL_HEIGHT = 2.5
|
const WALL_HEIGHT = 2.5
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ const updateWallPreview = (mesh: Mesh, start: Vector3, end: Vector3) => {
|
|||||||
const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
|
const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
|
||||||
const length = direction.length()
|
const length = direction.length()
|
||||||
|
|
||||||
if (length < 0.01) {
|
if (length < WALL_MIN_LENGTH) {
|
||||||
mesh.visible = false
|
mesh.visible = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,7 +143,7 @@ export const WallTool: React.FC = () => {
|
|||||||
endingPoint.current.set(snappedEnd[0], event.position[1], snappedEnd[1])
|
endingPoint.current.set(snappedEnd[0], event.position[1], snappedEnd[1])
|
||||||
const dx = endingPoint.current.x - startingPoint.current.x
|
const dx = endingPoint.current.x - startingPoint.current.x
|
||||||
const dz = endingPoint.current.z - startingPoint.current.z
|
const dz = endingPoint.current.z - startingPoint.current.z
|
||||||
if (dx * dx + dz * dz < 0.01 * 0.01) return
|
if (dx * dx + dz * dz < WALL_MIN_LENGTH * WALL_MIN_LENGTH) return
|
||||||
createWallOnCurrentLevel(
|
createWallOnCurrentLevel(
|
||||||
[startingPoint.current.x, startingPoint.current.z],
|
[startingPoint.current.x, startingPoint.current.z],
|
||||||
[endingPoint.current.x, endingPoint.current.z],
|
[endingPoint.current.x, endingPoint.current.z],
|
||||||
@@ -166,6 +167,7 @@ export const WallTool: React.FC = () => {
|
|||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
if (buildingState.current === 1) {
|
if (buildingState.current === 1) {
|
||||||
|
markToolCancelConsumed()
|
||||||
buildingState.current = 0
|
buildingState.current = 0
|
||||||
wallPreviewRef.current.visible = false
|
wallPreviewRef.current.visible = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export function SliderControl({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
min = 0,
|
min = Number.NEGATIVE_INFINITY,
|
||||||
max = 100,
|
max = Number.POSITIVE_INFINITY,
|
||||||
precision = 0,
|
precision = 0,
|
||||||
step = 1,
|
step = 1,
|
||||||
className,
|
className,
|
||||||
@@ -32,23 +32,12 @@ export function SliderControl({
|
|||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const [inputValue, setInputValue] = useState(value.toFixed(precision))
|
const [inputValue, setInputValue] = useState(value.toFixed(precision))
|
||||||
|
|
||||||
// Track the original value and bounds when dragging starts
|
const dragRef = useRef<{ startX: number; startValue: number } | null>(null)
|
||||||
const [dragStartValue, setDragStartValue] = useState<number | null>(null)
|
const labelRef = useRef<HTMLDivElement>(null)
|
||||||
const [dragMin, setDragMin] = useState<number | null>(null)
|
|
||||||
const [dragMax, setDragMax] = useState<number | null>(null)
|
|
||||||
|
|
||||||
const trackRef = useRef<HTMLDivElement>(null)
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const valueRef = useRef(value)
|
const valueRef = useRef(value)
|
||||||
valueRef.current = value
|
valueRef.current = value
|
||||||
|
|
||||||
const clamp = useCallback(
|
const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
|
||||||
(val: number) => {
|
|
||||||
return Math.min(Math.max(val, min), max)
|
|
||||||
},
|
|
||||||
[min, max],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
@@ -56,123 +45,91 @@ export function SliderControl({
|
|||||||
}
|
}
|
||||||
}, [value, precision, isEditing])
|
}, [value, precision, isEditing])
|
||||||
|
|
||||||
|
// Wheel support on the label
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current
|
const el = labelRef.current
|
||||||
if (!container) return
|
if (!el) return
|
||||||
|
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
if (isEditing) return
|
if (isEditing) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const direction = e.deltaY < 0 ? 1 : -1
|
const direction = e.deltaY < 0 ? 1 : -1
|
||||||
let scrollStep = step
|
let s = step
|
||||||
if (e.shiftKey) scrollStep = step * 10
|
if (e.shiftKey) s = step * 10
|
||||||
else if (e.altKey) scrollStep = step * 0.1
|
else if (e.altKey) s = step * 0.1
|
||||||
|
const newValue = clamp(valueRef.current + direction * s)
|
||||||
const newValue = clamp(valueRef.current + direction * scrollStep)
|
const final = Number.parseFloat(newValue.toFixed(precision))
|
||||||
const finalValue = Number.parseFloat(newValue.toFixed(precision))
|
if (final !== valueRef.current) onChange(final)
|
||||||
|
|
||||||
if (finalValue !== valueRef.current) {
|
|
||||||
onChange(finalValue)
|
|
||||||
}
|
}
|
||||||
}
|
el.addEventListener('wheel', handleWheel, { passive: false })
|
||||||
|
return () => el.removeEventListener('wheel', handleWheel)
|
||||||
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
||||||
return () => container.removeEventListener('wheel', handleWheel)
|
|
||||||
}, [isEditing, step, clamp, onChange, precision])
|
}, [isEditing, step, clamp, onChange, precision])
|
||||||
|
|
||||||
|
// Arrow key support while hovered
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isHovered || isEditing) return
|
if (!isHovered || isEditing) return
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
let direction = 0
|
let direction = 0
|
||||||
if (e.key === 'ArrowUp') direction = 1
|
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') direction = 1
|
||||||
else if (e.key === 'ArrowDown') direction = -1
|
else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') direction = -1
|
||||||
|
|
||||||
if (direction !== 0) {
|
if (direction !== 0) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
let scrollStep = step
|
let s = step
|
||||||
if (e.shiftKey) scrollStep = step * 10
|
if (e.shiftKey) s = step * 10
|
||||||
else if (e.altKey) scrollStep = step * 0.1
|
else if (e.metaKey || e.ctrlKey) s = step * 0.1
|
||||||
|
const newValue = clamp(valueRef.current + direction * s)
|
||||||
const newValue = clamp(valueRef.current + direction * scrollStep)
|
const final = Number.parseFloat(newValue.toFixed(precision))
|
||||||
const finalValue = Number.parseFloat(newValue.toFixed(precision))
|
if (final !== valueRef.current) onChange(final)
|
||||||
|
|
||||||
if (finalValue !== valueRef.current) {
|
|
||||||
onChange(finalValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [isHovered, isEditing, step, clamp, onChange, precision])
|
}, [isHovered, isEditing, step, clamp, onChange, precision])
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handleLabelPointerDown = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
if (isEditing) return
|
if (isEditing) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
const track = trackRef.current
|
dragRef.current = { startX: e.clientX, startValue: valueRef.current }
|
||||||
if (!track) return
|
|
||||||
|
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
setDragStartValue(value)
|
|
||||||
setDragMin(min)
|
|
||||||
setDragMax(max)
|
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
|
},
|
||||||
|
[isEditing],
|
||||||
|
)
|
||||||
|
|
||||||
const rect = track.getBoundingClientRect()
|
const handleLabelPointerMove = useCallback(
|
||||||
const updateValueFromEvent = (clientX: number) => {
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
|
if (!dragRef.current) return
|
||||||
const rawValue = min + percent * (max - min)
|
const { startX, startValue } = dragRef.current
|
||||||
// snap to step
|
const dx = e.clientX - startX
|
||||||
const snapped = Math.round(rawValue / step) * step
|
let s = step
|
||||||
const finalValue = Number.parseFloat(clamp(snapped).toFixed(precision))
|
if (e.shiftKey) s = step * 10
|
||||||
onChange(finalValue)
|
else if (e.metaKey || e.ctrlKey) s = step * 0.1
|
||||||
}
|
// 4 px per step at default sensitivity
|
||||||
|
const newValue = clamp(Number.parseFloat((startValue + (dx / 4) * s).toFixed(precision)))
|
||||||
|
onChange(newValue)
|
||||||
|
},
|
||||||
|
[step, precision, clamp, onChange],
|
||||||
|
)
|
||||||
|
|
||||||
updateValueFromEvent(e.clientX)
|
const handleLabelPointerUp = useCallback(
|
||||||
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
if (!dragRef.current) return
|
||||||
updateValueFromEvent(moveEvent.clientX)
|
const { startValue } = dragRef.current
|
||||||
}
|
|
||||||
|
|
||||||
const handlePointerUp = (e: PointerEvent) => {
|
|
||||||
// Only stop dragging if we didn't release on the reset button
|
|
||||||
// Let the reset button's onPointerDown handle its own cleanup
|
|
||||||
if ((e.target as HTMLElement).closest('button')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDragging(false)
|
|
||||||
const startVal = dragStartValue
|
|
||||||
const finalVal = valueRef.current
|
const finalVal = valueRef.current
|
||||||
|
dragRef.current = null
|
||||||
|
setIsDragging(false)
|
||||||
|
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||||
|
|
||||||
setDragStartValue(null)
|
if (startValue !== finalVal) {
|
||||||
setDragMin(null)
|
onChange(startValue)
|
||||||
setDragMax(null)
|
|
||||||
document.removeEventListener('pointermove', handlePointerMove)
|
|
||||||
document.removeEventListener('pointerup', handlePointerUp)
|
|
||||||
|
|
||||||
if (startVal !== null && startVal !== finalVal) {
|
|
||||||
// Revert to start value while paused so the undo baseline is clean
|
|
||||||
onChange(startVal)
|
|
||||||
|
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
|
|
||||||
// Apply final value while recording
|
|
||||||
onChange(finalVal)
|
onChange(finalVal)
|
||||||
} else {
|
} else {
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('pointermove', handlePointerMove)
|
|
||||||
document.addEventListener('pointerup', handlePointerUp)
|
|
||||||
},
|
},
|
||||||
[isEditing, min, max, step, precision, clamp, onChange, dragStartValue, value],
|
[onChange],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleValueClick = useCallback(() => {
|
const handleValueClick = useCallback(() => {
|
||||||
@@ -180,10 +137,6 @@ export function SliderControl({
|
|||||||
setInputValue(value.toFixed(precision))
|
setInputValue(value.toFixed(precision))
|
||||||
}, [value, precision])
|
}, [value, precision])
|
||||||
|
|
||||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setInputValue(e.target.value)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const submitValue = useCallback(() => {
|
const submitValue = useCallback(() => {
|
||||||
const numValue = Number.parseFloat(inputValue)
|
const numValue = Number.parseFloat(inputValue)
|
||||||
if (Number.isNaN(numValue)) {
|
if (Number.isNaN(numValue)) {
|
||||||
@@ -194,10 +147,6 @@ export function SliderControl({
|
|||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
}, [inputValue, onChange, clamp, precision, value])
|
}, [inputValue, onChange, clamp, precision, value])
|
||||||
|
|
||||||
const handleInputBlur = useCallback(() => {
|
|
||||||
submitValue()
|
|
||||||
}, [submitValue])
|
|
||||||
|
|
||||||
const handleInputKeyDown = useCallback(
|
const handleInputKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
@@ -220,104 +169,61 @@ export function SliderControl({
|
|||||||
[submitValue, value, precision, step, clamp, onChange],
|
[submitValue, value, precision, step, clamp, onChange],
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentMin = isDragging && dragMin !== null ? dragMin : min
|
|
||||||
const currentMax = isDragging && dragMax !== null ? dragMax : max
|
|
||||||
|
|
||||||
const percent = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(100, ((value - currentMin) / (currentMax - currentMin)) * 100),
|
|
||||||
)
|
|
||||||
const startPercent =
|
|
||||||
dragStartValue !== null
|
|
||||||
? Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(100, ((dragStartValue - currentMin) / (currentMax - currentMin)) * 100),
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex h-12 w-full items-center rounded-lg border border-border/50 px-3 text-sm transition-colors',
|
'group flex h-7 w-full select-none items-center rounded-lg px-2 transition-colors',
|
||||||
isDragging ? 'bg-[#3e3e3e]' : 'bg-[#2C2C2E] hover:bg-[#3e3e3e]',
|
isDragging ? 'bg-white/5' : 'hover:bg-white/5',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
ref={containerRef}
|
|
||||||
>
|
>
|
||||||
{/* Reset button that appears when dragged away from start */}
|
{/* Label — drag handle */}
|
||||||
{isDragging && dragStartValue !== null && dragStartValue !== value && (
|
|
||||||
<button
|
|
||||||
className="pointer-events-auto absolute -top-10 right-0 z-50 cursor-pointer rounded-md bg-[#2C2C2E] px-2 py-1 font-medium text-[10px] text-muted-foreground shadow-sm ring-1 ring-border/50 hover:bg-[#3e3e3e] hover:text-foreground"
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onChange(dragStartValue)
|
|
||||||
setDragStartValue(null)
|
|
||||||
setDragMin(null)
|
|
||||||
setDragMax(null)
|
|
||||||
setIsDragging(false)
|
|
||||||
useScene.temporal.getState().resume()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="w-[80px] shrink-0 select-none truncate text-muted-foreground">{label}</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative mx-2 flex h-full flex-1 touch-none items-center justify-center',
|
'flex shrink-0 cursor-ew-resize items-center gap-1.5 text-xs transition-colors',
|
||||||
isDragging ? 'cursor-grabbing' : 'cursor-grab',
|
isDragging ? 'text-foreground' : 'text-muted-foreground hover:text-foreground/80',
|
||||||
)}
|
)}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handleLabelPointerDown}
|
||||||
ref={trackRef}
|
onPointerMove={handleLabelPointerMove}
|
||||||
|
onPointerUp={handleLabelPointerUp}
|
||||||
|
ref={labelRef}
|
||||||
>
|
>
|
||||||
{/* Track dots background */}
|
{/* Grip dots — 2×3 grid */}
|
||||||
<div className="pointer-events-none absolute inset-x-0 flex items-center justify-between px-1 opacity-30">
|
<div
|
||||||
{[...Array(9)].map((_, i) => (
|
className={cn(
|
||||||
<div className="h-[3px] w-[3px] rounded-full bg-current" key={i} />
|
'grid grid-cols-2 gap-[2.5px] transition-opacity',
|
||||||
|
isDragging ? 'opacity-70' : 'opacity-25 group-hover:opacity-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<div className="h-[2px] w-[2px] rounded-full bg-current" key={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<span className="font-medium">{label}</span>
|
||||||
{/* Original Thumb Ghost */}
|
|
||||||
{isDragging && startPercent !== null && (
|
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute top-1/2 h-6 w-[3px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground/20 shadow-sm"
|
|
||||||
style={{ left: `${startPercent}%` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Active Thumb */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'pointer-events-none absolute top-1/2 h-6 w-[3px] -translate-x-1/2 -translate-y-1/2 rounded-full shadow-sm transition',
|
|
||||||
isDragging
|
|
||||||
? 'scale-y-110 bg-foreground'
|
|
||||||
: 'bg-foreground/60 group-hover:bg-foreground/80',
|
|
||||||
)}
|
|
||||||
style={{ left: `${percent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-[50px] shrink-0 justify-end">
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Value — click to edit */}
|
||||||
|
<div className="flex items-center text-xs">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="flex items-center">
|
<>
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full bg-transparent p-0 text-right font-mono text-foreground outline-none selection:bg-primary/30"
|
className="w-14 bg-transparent p-0 text-right font-mono text-foreground outline-none selection:bg-primary/30"
|
||||||
onBlur={handleInputBlur}
|
onBlur={submitValue}
|
||||||
onChange={handleInputChange}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
type="text"
|
type="text"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
/>
|
/>
|
||||||
{unit && <span className="ml-[1px] text-muted-foreground">{unit}</span>}
|
{unit && <span className="ml-[1px] text-muted-foreground">{unit}</span>}
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="flex w-full cursor-text items-center justify-end text-foreground/60 transition-colors hover:text-foreground"
|
className="flex cursor-text items-center text-foreground/60 transition-colors hover:text-foreground"
|
||||||
onClick={handleValueClick}
|
onClick={handleValueClick}
|
||||||
>
|
>
|
||||||
<span className="font-mono tabular-nums tracking-tight">
|
<span className="font-mono tabular-nums tracking-tight">
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
|||||||
alt={item.name}
|
alt={item.name}
|
||||||
className="rounded-lg object-cover"
|
className="rounded-lg object-cover"
|
||||||
fill
|
fill
|
||||||
|
loading="eager"
|
||||||
|
sizes="56px"
|
||||||
src={resolveCdnUrl(item.thumbnail) || ''}
|
src={resolveCdnUrl(item.thumbnail) || ''}
|
||||||
/>
|
/>
|
||||||
{attachmentIcon && (
|
{attachmentIcon && (
|
||||||
|
|||||||
@@ -25,6 +25,29 @@ export function WallPanel() {
|
|||||||
[selectedId, updateNode],
|
[selectedId, updateNode],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Função mágica para a Issue #191: Atualiza o comprimento via cálculo vetorial
|
||||||
|
const handleUpdateLength = useCallback((newLength: number) => {
|
||||||
|
if (!node || newLength <= 0) return
|
||||||
|
|
||||||
|
const dx = node.end[0] - node.start[0]
|
||||||
|
const dz = node.end[1] - node.start[1]
|
||||||
|
const currentLength = Math.sqrt(dx * dx + dz * dz)
|
||||||
|
|
||||||
|
if (currentLength === 0) return
|
||||||
|
|
||||||
|
// Calcula a direção (vetor unitário)
|
||||||
|
const dirX = dx / currentLength
|
||||||
|
const dirZ = dz / currentLength
|
||||||
|
|
||||||
|
// Define o novo ponto final baseado no novo comprimento
|
||||||
|
const newEnd: [number, number] = [
|
||||||
|
node.start[0] + dirX * newLength,
|
||||||
|
node.start[1] + dirZ * newLength
|
||||||
|
]
|
||||||
|
|
||||||
|
handleUpdate({ end: newEnd })
|
||||||
|
}, [node, handleUpdate])
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [setSelection])
|
}, [setSelection])
|
||||||
@@ -46,6 +69,17 @@ export function WallPanel() {
|
|||||||
width={280}
|
width={280}
|
||||||
>
|
>
|
||||||
<PanelSection title="Dimensions">
|
<PanelSection title="Dimensions">
|
||||||
|
{/* Adicionando o controle de Length solicitado na Issue #191 */}
|
||||||
|
<SliderControl
|
||||||
|
label="Length"
|
||||||
|
max={20}
|
||||||
|
min={0.1}
|
||||||
|
onChange={handleUpdateLength}
|
||||||
|
precision={2}
|
||||||
|
step={0.01}
|
||||||
|
unit="m"
|
||||||
|
value={length}
|
||||||
|
/>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Height"
|
label="Height"
|
||||||
max={6}
|
max={6}
|
||||||
@@ -67,13 +101,6 @@ export function WallPanel() {
|
|||||||
value={Math.round(thickness * 1000) / 1000}
|
value={Math.round(thickness * 1000) / 1000}
|
||||||
/>
|
/>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
|
||||||
<PanelSection title="Info">
|
|
||||||
<div className="flex items-center justify-between px-2 py-1 text-muted-foreground text-sm">
|
|
||||||
<span>Length</span>
|
|
||||||
<span className="font-mono text-white">{length.toFixed(2)} m</span>
|
|
||||||
</div>
|
|
||||||
</PanelSection>
|
|
||||||
</PanelWrapper>
|
</PanelWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,8 +209,6 @@ export function WindowPanel() {
|
|||||||
X<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
|
X<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
max={10}
|
|
||||||
min={-10}
|
|
||||||
onChange={(v) => handleUpdate({ position: [v, node.position[1], node.position[2]] })}
|
onChange={(v) => handleUpdate({ position: [v, node.position[1], node.position[2]] })}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
@@ -223,8 +221,6 @@ export function WindowPanel() {
|
|||||||
Y<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
|
Y<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
max={10}
|
|
||||||
min={-10}
|
|
||||||
onChange={(v) => handleUpdate({ position: [node.position[0], v, node.position[2]] })}
|
onChange={(v) => handleUpdate({ position: [node.position[0], v, node.position[2]] })}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
@@ -244,8 +240,7 @@ export function WindowPanel() {
|
|||||||
<PanelSection title="Dimensions">
|
<PanelSection title="Dimensions">
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Width"
|
label="Width"
|
||||||
max={5}
|
min={0}
|
||||||
min={0.2}
|
|
||||||
onChange={(v) => handleUpdate({ width: v })}
|
onChange={(v) => handleUpdate({ width: v })}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
@@ -254,8 +249,7 @@ export function WindowPanel() {
|
|||||||
/>
|
/>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Height"
|
label="Height"
|
||||||
max={5}
|
min={0}
|
||||||
min={0.2}
|
|
||||||
onChange={(v) => handleUpdate({ height: v })}
|
onChange={(v) => handleUpdate({ height: v })}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
@@ -267,8 +261,7 @@ export function WindowPanel() {
|
|||||||
<PanelSection title="Frame">
|
<PanelSection title="Frame">
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Thickness"
|
label="Thickness"
|
||||||
max={0.2}
|
min={0}
|
||||||
min={0.01}
|
|
||||||
onChange={(v) => handleUpdate({ frameThickness: v })}
|
onChange={(v) => handleUpdate({ frameThickness: v })}
|
||||||
precision={3}
|
precision={3}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
@@ -277,8 +270,7 @@ export function WindowPanel() {
|
|||||||
/>
|
/>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Depth"
|
label="Depth"
|
||||||
max={0.3}
|
min={0}
|
||||||
min={0.01}
|
|
||||||
onChange={(v) => handleUpdate({ frameDepth: v })}
|
onChange={(v) => handleUpdate({ frameDepth: v })}
|
||||||
precision={3}
|
precision={3}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
@@ -390,8 +382,7 @@ export function WindowPanel() {
|
|||||||
<div className="mt-1 flex flex-col gap-1">
|
<div className="mt-1 flex flex-col gap-1">
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Depth"
|
label="Depth"
|
||||||
max={0.5}
|
min={0}
|
||||||
min={0.01}
|
|
||||||
onChange={(v) => handleUpdate({ sillDepth: v })}
|
onChange={(v) => handleUpdate({ sillDepth: v })}
|
||||||
precision={3}
|
precision={3}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
@@ -400,8 +391,7 @@ export function WindowPanel() {
|
|||||||
/>
|
/>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Thickness"
|
label="Thickness"
|
||||||
max={0.2}
|
min={0}
|
||||||
min={0.005}
|
|
||||||
onChange={(v) => handleUpdate({ sillThickness: v })}
|
onChange={(v) => handleUpdate({ sillThickness: v })}
|
||||||
precision={3}
|
precision={3}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
|||||||
@@ -202,9 +202,9 @@ export function SettingsPanel({
|
|||||||
|
|
||||||
const isLocalProject = false // Props-based; only show cloud sections when projectId provided
|
const isLocalProject = false // Props-based; only show cloud sections when projectId provided
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async (format: 'glb' | 'stl' | 'obj' = 'glb') => {
|
||||||
if (exportScene) {
|
if (exportScene) {
|
||||||
await exportScene()
|
await exportScene(format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,9 +318,17 @@ export function SettingsPanel({
|
|||||||
{/* Export Section */}
|
{/* Export Section */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
|
<label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
|
||||||
<Button className="w-full justify-start gap-2" onClick={handleExport} variant="outline">
|
<Button className="w-full justify-start gap-2" onClick={() => handleExport('glb')} variant="outline">
|
||||||
<Download className="size-4" />
|
<Download className="size-4" />
|
||||||
Export 3D Model
|
Export as GLB
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start gap-2" onClick={() => handleExport('stl')} variant="outline">
|
||||||
|
<Download className="size-4" />
|
||||||
|
Export as STL
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start gap-2" onClick={() => handleExport('obj')} variant="outline">
|
||||||
|
<Download className="size-4" />
|
||||||
|
Export as OBJ
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import { useEffect } from 'react'
|
|||||||
import { sfxEmitter } from '../lib/sfx-bus'
|
import { sfxEmitter } from '../lib/sfx-bus'
|
||||||
import useEditor from '../store/use-editor'
|
import useEditor from '../store/use-editor'
|
||||||
|
|
||||||
|
// Tools call this in their onCancel handler when they have an active mid-action to cancel,
|
||||||
|
// so that the global Escape handler knows not to also switch to select mode.
|
||||||
|
let _toolCancelConsumed = false
|
||||||
|
export const markToolCancelConsumed = () => {
|
||||||
|
_toolCancelConsumed = true
|
||||||
|
}
|
||||||
|
|
||||||
export const useKeyboard = () => {
|
export const useKeyboard = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -14,8 +21,12 @@ export const useKeyboard = () => {
|
|||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
_toolCancelConsumed = false
|
||||||
emitter.emit('tool:cancel')
|
emitter.emit('tool:cancel')
|
||||||
|
|
||||||
|
// Only switch to select mode if no tool had an active mid-action to cancel.
|
||||||
|
// (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
|
||||||
|
if (!_toolCancelConsumed) {
|
||||||
// Return to the default select tool while keeping the active building/level context.
|
// Return to the default select tool while keeping the active building/level context.
|
||||||
useEditor.getState().setEditingHole(null)
|
useEditor.getState().setEditingHole(null)
|
||||||
useEditor.getState().setMode('select')
|
useEditor.getState().setMode('select')
|
||||||
@@ -23,6 +34,7 @@ export const useKeyboard = () => {
|
|||||||
// Clear selections to close UI panels, but KEEP the active building and level context.
|
// Clear selections to close UI panels, but KEEP the active building and level context.
|
||||||
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
||||||
useEditor.getState().setSelectedReferenceId(null)
|
useEditor.getState().setSelectedReferenceId(null)
|
||||||
|
}
|
||||||
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useEditor.getState().setPhase('site')
|
useEditor.getState().setPhase('site')
|
||||||
@@ -50,6 +62,13 @@ export const useKeyboard = () => {
|
|||||||
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useEditor.getState().setMode('select')
|
useEditor.getState().setMode('select')
|
||||||
|
} else if (e.key === 'd' && !e.metaKey && !e.ctrlKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
const phase = useEditor.getState().phase
|
||||||
|
if (phase === 'structure' || phase === 'furnish') {
|
||||||
|
useEditor.getState().setMode('delete')
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [] })
|
||||||
|
}
|
||||||
} else if (e.key === 'b' && !e.metaKey && !e.ctrlKey) {
|
} else if (e.key === 'b' && !e.metaKey && !e.ctrlKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useEditor.getState().setMode('build')
|
useEditor.getState().setMode('build')
|
||||||
@@ -114,6 +133,25 @@ export const useKeyboard = () => {
|
|||||||
sfxEmitter.emit('sfx:item-rotate') // Play a sound for feedback
|
sfxEmitter.emit('sfx:item-rotate') // Play a sound for feedback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (e.key === 't' || e.key === 'T') {
|
||||||
|
// Rotate selected node counter-clockwise
|
||||||
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
||||||
|
if (selectedNodeIds.length === 1) {
|
||||||
|
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
||||||
|
if (node && 'rotation' in node) {
|
||||||
|
e.preventDefault()
|
||||||
|
const ROTATION_STEP = Math.PI / 4
|
||||||
|
|
||||||
|
if (typeof node.rotation === 'number') {
|
||||||
|
useScene.getState().updateNode(node.id, { rotation: node.rotation - ROTATION_STEP })
|
||||||
|
} else if (Array.isArray(node.rotation)) {
|
||||||
|
useScene.getState().updateNode(node.id, {
|
||||||
|
rotation: [node.rotation[0], node.rotation[1] - ROTATION_STEP, node.rotation[2]],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sfxEmitter.emit('sfx:item-rotate')
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pascal-app/viewer",
|
"name": "@pascal-app/viewer",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "3D viewer component for Pascal building editor",
|
"description": "3D viewer component for Pascal building editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { ScanSystem } from '../../systems/scan/scan-system'
|
|||||||
import { WallCutout } from '../../systems/wall/wall-cutout'
|
import { WallCutout } from '../../systems/wall/wall-cutout'
|
||||||
import { ZoneSystem } from '../../systems/zone/zone-system'
|
import { ZoneSystem } from '../../systems/zone/zone-system'
|
||||||
import { SceneRenderer } from '../renderers/scene-renderer'
|
import { SceneRenderer } from '../renderers/scene-renderer'
|
||||||
import { GroundOccluder } from './ground-occluder'
|
|
||||||
import { Lights } from './lights'
|
import { Lights } from './lights'
|
||||||
import { PerfMonitor } from './perf-monitor'
|
import { PerfMonitor } from './perf-monitor'
|
||||||
import PostProcessing from './post-processing'
|
import PostProcessing from './post-processing'
|
||||||
@@ -110,15 +109,18 @@ const Viewer: React.FC<ViewerProps> = ({
|
|||||||
const renderer = new THREE.WebGPURenderer(props as any)
|
const renderer = new THREE.WebGPURenderer(props as any)
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||||
renderer.toneMappingExposure = 0.9
|
renderer.toneMappingExposure = 0.9
|
||||||
|
// renderer.init() // Only use when using <DebugRenderer />
|
||||||
return renderer
|
return renderer
|
||||||
}}
|
}}
|
||||||
|
resize={{
|
||||||
|
debounce: 100,
|
||||||
|
}}
|
||||||
shadows={{
|
shadows={{
|
||||||
type: THREE.PCFShadowMap,
|
type: THREE.PCFShadowMap,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <AnimatedBackground isDark={theme === 'dark'} /> */}
|
{/* <AnimatedBackground isDark={theme === 'dark'} /> */}
|
||||||
<GroundOccluder />
|
|
||||||
<ViewerCamera />
|
<ViewerCamera />
|
||||||
|
|
||||||
{/* <directionalLight position={[10, 10, 5]} intensity={0.5} castShadow
|
{/* <directionalLight position={[10, 10, 5]} intensity={0.5} castShadow
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import { Color, Layers, UnsignedByteType } from 'three'
|
import { Color, Layers, UnsignedByteType } from 'three'
|
||||||
import { outline } from 'three/addons/tsl/display/OutlineNode.js'
|
import { outline } from 'three/addons/tsl/display/OutlineNode.js'
|
||||||
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
||||||
import { traa } from 'three/addons/tsl/display/TRAANode.js'
|
|
||||||
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
@@ -21,7 +20,6 @@ import {
|
|||||||
time,
|
time,
|
||||||
uniform,
|
uniform,
|
||||||
vec4,
|
vec4,
|
||||||
velocity,
|
|
||||||
} from 'three/tsl'
|
} from 'three/tsl'
|
||||||
import { RenderPipeline, type WebGPURenderer } from 'three/webgpu'
|
import { RenderPipeline, type WebGPURenderer } from 'three/webgpu'
|
||||||
import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers'
|
import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers'
|
||||||
@@ -129,41 +127,45 @@ const PostProcessingPasses = () => {
|
|||||||
outliner.hoveredObjects.length = 0
|
outliner.hoveredObjects.length = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Scene pass with MRT for SSGI
|
|
||||||
const scenePass = pass(scene, camera)
|
const scenePass = pass(scene, camera)
|
||||||
|
const zonePass = pass(scene, camera)
|
||||||
|
zonePass.setLayers(zoneLayers)
|
||||||
|
|
||||||
|
const scenePassColor = scenePass.getTextureNode('output')
|
||||||
|
|
||||||
|
// Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
|
||||||
|
// so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
|
||||||
|
// WebGPU only applies clearColorValue to MRT attachment 0 (output), so scenePassColor.a
|
||||||
|
// is the reliable geometry mask — no normals, no flicker.
|
||||||
|
const hasGeometry = scenePassColor.a
|
||||||
|
const contentAlpha = hasGeometry.max(zonePass.a)
|
||||||
|
|
||||||
|
let sceneColor = scenePassColor as unknown as ReturnType<typeof vec4>
|
||||||
|
|
||||||
|
if (SSGI_PARAMS.enabled) {
|
||||||
|
// MRT only needed for SSGI (diffuse for GI, normal for SSGI sampling)
|
||||||
scenePass.setMRT(
|
scenePass.setMRT(
|
||||||
mrt({
|
mrt({
|
||||||
output,
|
output,
|
||||||
diffuseColor,
|
diffuseColor,
|
||||||
normal: directionToColor(normalView),
|
normal: directionToColor(normalView),
|
||||||
velocity,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get texture outputs
|
|
||||||
const scenePassColor = scenePass.getTextureNode('output')
|
|
||||||
const scenePassDiffuse = scenePass.getTextureNode('diffuseColor')
|
const scenePassDiffuse = scenePass.getTextureNode('diffuseColor')
|
||||||
const scenePassDepth = scenePass.getTextureNode('depth')
|
const scenePassDepth = scenePass.getTextureNode('depth')
|
||||||
const scenePassNormal = scenePass.getTextureNode('normal')
|
const scenePassNormal = scenePass.getTextureNode('normal')
|
||||||
const scenePassVelocity = scenePass.getTextureNode('velocity')
|
|
||||||
|
|
||||||
// Optimize texture bandwidth
|
// Optimize texture bandwidth
|
||||||
const diffuseTexture = scenePass.getTexture('diffuseColor')
|
const diffuseTexture = scenePass.getTexture('diffuseColor')
|
||||||
diffuseTexture.type = UnsignedByteType
|
diffuseTexture.type = UnsignedByteType
|
||||||
|
|
||||||
const normalTexture = scenePass.getTexture('normal')
|
const normalTexture = scenePass.getTexture('normal')
|
||||||
normalTexture.type = UnsignedByteType
|
normalTexture.type = UnsignedByteType
|
||||||
|
|
||||||
// Extract normal from color-encoded texture
|
// Extract normal from color-encoded texture
|
||||||
const sceneNormal = sample((uv) => {
|
const sceneNormal = sample((uv) => colorToDirection(scenePassNormal.sample(uv)))
|
||||||
return colorToDirection(scenePassNormal.sample(uv))
|
|
||||||
})
|
|
||||||
|
|
||||||
const zonePass = pass(scene, camera)
|
|
||||||
zonePass.setLayers(zoneLayers)
|
|
||||||
// SSGI Pass (cast to PerspectiveCamera for SSGI)
|
|
||||||
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera as any)
|
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera as any)
|
||||||
|
|
||||||
giPass.sliceCount.value = SSGI_PARAMS.sliceCount
|
giPass.sliceCount.value = SSGI_PARAMS.sliceCount
|
||||||
giPass.stepCount.value = SSGI_PARAMS.stepCount
|
giPass.stepCount.value = SSGI_PARAMS.stepCount
|
||||||
giPass.radius.value = SSGI_PARAMS.radius
|
giPass.radius.value = SSGI_PARAMS.radius
|
||||||
@@ -180,7 +182,6 @@ const PostProcessingPasses = () => {
|
|||||||
|
|
||||||
// DenoiseNode only denoises RGB — alpha is passed through unchanged.
|
// DenoiseNode only denoises RGB — alpha is passed through unchanged.
|
||||||
// SSGI packs AO into alpha, so we remap it into RGB before denoising.
|
// SSGI packs AO into alpha, so we remap it into RGB before denoising.
|
||||||
// convertToTexture() inside denoise() will call rtt() on this vec4 node automatically.
|
|
||||||
const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1))
|
const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1))
|
||||||
const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera)
|
const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera)
|
||||||
denoisePass.index.value = 0
|
denoisePass.index.value = 0
|
||||||
@@ -188,21 +189,13 @@ const PostProcessingPasses = () => {
|
|||||||
|
|
||||||
const gi = giPass.rgb
|
const gi = giPass.rgb
|
||||||
const ao = (denoisePass as any).r
|
const ao = (denoisePass as any).r
|
||||||
// const gi = giPass.rgb;
|
|
||||||
// const ao = giPass.a;
|
|
||||||
|
|
||||||
// Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
|
|
||||||
// so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
|
|
||||||
// WebGPU only applies clearColorValue to MRT attachment 0 (output), so scenePassColor.a
|
|
||||||
// is the reliable geometry mask — no normals, no flicker.
|
|
||||||
const hasGeometry = scenePassColor.a
|
|
||||||
const contentAlpha = hasGeometry.max(zonePass.a)
|
|
||||||
|
|
||||||
// Composite: scene * AO + diffuse * GI
|
// Composite: scene * AO + diffuse * GI
|
||||||
const compositePass = vec4(
|
sceneColor = vec4(
|
||||||
add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))),
|
add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))),
|
||||||
contentAlpha,
|
contentAlpha,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function generateSelectedOutlinePass() {
|
function generateSelectedOutlinePass() {
|
||||||
const edgeStrength = uniform(3)
|
const edgeStrength = uniform(3)
|
||||||
@@ -256,20 +249,15 @@ const PostProcessingPasses = () => {
|
|||||||
const selectedOutlinePass = generateSelectedOutlinePass()
|
const selectedOutlinePass = generateSelectedOutlinePass()
|
||||||
const hoverOutlinePass = generateHoverOutlinePass()
|
const hoverOutlinePass = generateHoverOutlinePass()
|
||||||
|
|
||||||
// Combine composite with outlines BEFORE applying TRAA
|
const compositeWithOutlines = vec4(
|
||||||
const compositeWithOutlines = SSGI_PARAMS.enabled
|
add(sceneColor.rgb, selectedOutlinePass.add(hoverOutlinePass)),
|
||||||
? vec4(add(compositePass.rgb, selectedOutlinePass.add(hoverOutlinePass)), compositePass.a)
|
sceneColor.a,
|
||||||
: vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a)
|
)
|
||||||
|
|
||||||
// TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
|
const finalOutput = vec4(
|
||||||
const traaOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera)
|
mix(bgUniform.current, compositeWithOutlines.rgb, contentAlpha),
|
||||||
|
float(1),
|
||||||
// For zone-over-background pixels, scenePassDepth=1.0 (no scene geometry) causes TRAA
|
)
|
||||||
// to output black. Use hasGeometry to blend: geometry pixels use traaRgb, all others
|
|
||||||
// (zones over background, pure background) use compositePass.rgb directly.
|
|
||||||
const traaRgb = (traaOutput as any).rgb
|
|
||||||
const colorSource = mix(compositePass.rgb, traaRgb, hasGeometry)
|
|
||||||
const finalOutput = vec4(mix(bgUniform.current, colorSource, contentAlpha), float(1))
|
|
||||||
|
|
||||||
const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
|
const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
|
||||||
renderPipeline.outputNode = finalOutput
|
renderPipeline.outputNode = finalOutput
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ type ViewerState = {
|
|||||||
setSelection: (updates: Partial<SelectionPath>) => void
|
setSelection: (updates: Partial<SelectionPath>) => void
|
||||||
resetSelection: () => void
|
resetSelection: () => void
|
||||||
outliner: Outliner
|
outliner: Outliner
|
||||||
exportScene: (() => Promise<void>) | null
|
exportScene: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null
|
||||||
setExportScene: (fn: (() => Promise<void>) | null) => void
|
setExportScene: (fn: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null) => void
|
||||||
}
|
}
|
||||||
declare const useViewer: import('zustand').UseBoundStore<import('zustand').StoreApi<ViewerState>>
|
declare const useViewer: import('zustand').UseBoundStore<import('zustand').StoreApi<ViewerState>>
|
||||||
export default useViewer
|
export default useViewer
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ type ViewerState = {
|
|||||||
outliner: Outliner // No setter as we will manipulate directly the arrays
|
outliner: Outliner // No setter as we will manipulate directly the arrays
|
||||||
|
|
||||||
// Export functionality
|
// Export functionality
|
||||||
exportScene: (() => Promise<void>) | null
|
exportScene: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null
|
||||||
setExportScene: (fn: (() => Promise<void>) | null) => void
|
setExportScene: (fn: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null) => void
|
||||||
|
|
||||||
debugColors: boolean
|
debugColors: boolean
|
||||||
setDebugColors: (enabled: boolean) => void
|
setDebugColors: (enabled: boolean) => void
|
||||||
|
|||||||
Reference in New Issue
Block a user