Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.
Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
@types/react, @types/react-dom, @types/three to prevent JSX namespace
fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
match the new versions on every bump (so viewer/editor/mcp tarballs always
reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts
Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
what private-editor currently reports
Open PRs against editor-v2 will need rebasing/conflict resolution.
This commit is contained in:
@@ -10,7 +10,9 @@ on:
|
|||||||
options:
|
options:
|
||||||
- core
|
- core
|
||||||
- viewer
|
- viewer
|
||||||
- both
|
- editor
|
||||||
|
- mcp
|
||||||
|
- all
|
||||||
bump:
|
bump:
|
||||||
description: "Version bump"
|
description: "Version bump"
|
||||||
required: true
|
required: true
|
||||||
@@ -51,56 +53,104 @@ jobs:
|
|||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
- name: Bump & publish core
|
- name: Bump versions and sync inter-package references
|
||||||
if: inputs.package == 'core' || inputs.package == 'both'
|
run: |
|
||||||
|
BUMP=${{ inputs.bump }}
|
||||||
|
TARGET=${{ inputs.package }}
|
||||||
|
|
||||||
|
bump_version() {
|
||||||
|
local v=$1
|
||||||
|
IFS='.' read -r MAJ MIN PAT <<< "$v"
|
||||||
|
if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi
|
||||||
|
if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi
|
||||||
|
if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi
|
||||||
|
echo "$MAJ.$MIN.$PAT"
|
||||||
|
}
|
||||||
|
|
||||||
|
for pkg in core viewer editor mcp; do
|
||||||
|
if [ "$TARGET" = "$pkg" ] || [ "$TARGET" = "all" ]; then
|
||||||
|
CUR=$(jq -r '.version' packages/$pkg/package.json)
|
||||||
|
NEW=$(bump_version "$CUR")
|
||||||
|
jq --arg v "$NEW" '.version = $v' packages/$pkg/package.json > tmp.json && mv tmp.json packages/$pkg/package.json
|
||||||
|
UPPER=$(echo "$pkg" | tr '[:lower:]' '[:upper:]')
|
||||||
|
echo "${UPPER}_VERSION=$NEW" >> $GITHUB_ENV
|
||||||
|
echo "Bumped @pascal-app/$pkg: $CUR → $NEW"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sync inter-package references in peerDependencies and devDependencies.
|
||||||
|
# Anything that references a bumped @pascal-app/* package is updated to ^NEW.
|
||||||
|
for pkg in core viewer editor mcp; do
|
||||||
|
FILE=packages/$pkg/package.json
|
||||||
|
for dep in core viewer editor mcp; do
|
||||||
|
UPPER=$(echo "$dep" | tr '[:lower:]' '[:upper:]')
|
||||||
|
VAR="${UPPER}_VERSION"
|
||||||
|
VAL="${!VAR}"
|
||||||
|
[ -z "$VAL" ] && continue
|
||||||
|
jq --arg name "@pascal-app/$dep" --arg v "^$VAL" '
|
||||||
|
if .peerDependencies[$name] then .peerDependencies[$name] = $v else . end
|
||||||
|
| if .devDependencies[$name] then .devDependencies[$name] = $v else . end
|
||||||
|
' "$FILE" > tmp.json && mv tmp.json "$FILE"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build & publish core
|
||||||
|
if: inputs.package == 'core' || inputs.package == 'all'
|
||||||
working-directory: packages/core
|
working-directory: packages/core
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
BUMP=${{ inputs.bump }}
|
|
||||||
VERSION=$(jq -r '.version' package.json)
|
|
||||||
IFS='.' read -r MAJ MIN PAT <<< "$VERSION"
|
|
||||||
if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi
|
|
||||||
if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi
|
|
||||||
if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi
|
|
||||||
VERSION="$MAJ.$MIN.$PAT"
|
|
||||||
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
|
|
||||||
echo "CORE_VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
if [ "${{ inputs.dry-run }}" = "true" ]; then
|
if [ "${{ inputs.dry-run }}" = "true" ]; then
|
||||||
echo "🏜️ Dry run — would publish @pascal-app/core@$VERSION"
|
echo "🏜️ Dry run — would publish @pascal-app/core@$CORE_VERSION"
|
||||||
npm publish --dry-run --access public
|
npm publish --dry-run --access public
|
||||||
else
|
else
|
||||||
npm publish --access public
|
npm publish --access public
|
||||||
echo "📦 Published @pascal-app/core@$VERSION"
|
echo "📦 Published @pascal-app/core@$CORE_VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump & publish viewer
|
- name: Build & publish viewer
|
||||||
if: inputs.package == 'viewer' || inputs.package == 'both'
|
if: inputs.package == 'viewer' || inputs.package == 'all'
|
||||||
working-directory: packages/viewer
|
working-directory: packages/viewer
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
BUMP=${{ inputs.bump }}
|
|
||||||
VERSION=$(jq -r '.version' package.json)
|
|
||||||
IFS='.' read -r MAJ MIN PAT <<< "$VERSION"
|
|
||||||
if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi
|
|
||||||
if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi
|
|
||||||
if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi
|
|
||||||
VERSION="$MAJ.$MIN.$PAT"
|
|
||||||
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
|
|
||||||
echo "VIEWER_VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
if [ "${{ inputs.dry-run }}" = "true" ]; then
|
if [ "${{ inputs.dry-run }}" = "true" ]; then
|
||||||
echo "🏜️ Dry run — would publish @pascal-app/viewer@$VERSION"
|
echo "🏜️ Dry run — would publish @pascal-app/viewer@$VIEWER_VERSION"
|
||||||
npm publish --dry-run --access public
|
npm publish --dry-run --access public
|
||||||
else
|
else
|
||||||
npm publish --access public
|
npm publish --access public
|
||||||
echo "📦 Published @pascal-app/viewer@$VERSION"
|
echo "📦 Published @pascal-app/viewer@$VIEWER_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish editor
|
||||||
|
if: inputs.package == 'editor' || inputs.package == 'all'
|
||||||
|
working-directory: packages/editor
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.dry-run }}" = "true" ]; then
|
||||||
|
echo "🏜️ Dry run — would publish @pascal-app/editor@$EDITOR_VERSION"
|
||||||
|
npm publish --dry-run --access public
|
||||||
|
else
|
||||||
|
npm publish --access public
|
||||||
|
echo "📦 Published @pascal-app/editor@$EDITOR_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build & publish mcp
|
||||||
|
if: inputs.package == 'mcp' || inputs.package == 'all'
|
||||||
|
working-directory: packages/mcp
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
bun run build
|
||||||
|
if [ "${{ inputs.dry-run }}" = "true" ]; then
|
||||||
|
echo "🏜️ Dry run — would publish @pascal-app/mcp@$MCP_VERSION"
|
||||||
|
npm publish --dry-run --access public
|
||||||
|
else
|
||||||
|
npm publish --access public
|
||||||
|
echo "📦 Published @pascal-app/mcp@$MCP_VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Commit version bumps & tag
|
- name: Commit version bumps & tag
|
||||||
@@ -118,6 +168,14 @@ jobs:
|
|||||||
PKGS="$PKGS @pascal-app/viewer@$VIEWER_VERSION"
|
PKGS="$PKGS @pascal-app/viewer@$VIEWER_VERSION"
|
||||||
TAGS="$TAGS @pascal-app/viewer@$VIEWER_VERSION"
|
TAGS="$TAGS @pascal-app/viewer@$VIEWER_VERSION"
|
||||||
fi
|
fi
|
||||||
|
if [ -n "$EDITOR_VERSION" ]; then
|
||||||
|
PKGS="$PKGS @pascal-app/editor@$EDITOR_VERSION"
|
||||||
|
TAGS="$TAGS @pascal-app/editor@$EDITOR_VERSION"
|
||||||
|
fi
|
||||||
|
if [ -n "$MCP_VERSION" ]; then
|
||||||
|
PKGS="$PKGS @pascal-app/mcp@$MCP_VERSION"
|
||||||
|
TAGS="$TAGS @pascal-app/mcp@$MCP_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
git commit -m "release:${PKGS}"
|
git commit -m "release:${PKGS}"
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ function parseIfMatch(raw: string | null): number | undefined {
|
|||||||
const inner = match ? match[1] : trimmed
|
const inner = match ? match[1] : trimmed
|
||||||
if (!inner) return undefined
|
if (!inner) return undefined
|
||||||
const n = Number(inner)
|
const n = Number(inner)
|
||||||
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) return undefined
|
if (!(Number.isFinite(n) && Number.isInteger(n)) || n < 0) return undefined
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "../../../styles/elevation.css";
|
||||||
@source "../../../packages/editor/src";
|
@source "../../../packages/editor/src";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Editor, type SidebarTab, ViewerToolbarLeft, ViewerToolbarRight } from '@pascal-app/editor'
|
import {
|
||||||
|
Editor,
|
||||||
|
ItemsPanel,
|
||||||
|
type SidebarTab,
|
||||||
|
ViewerToolbarLeft,
|
||||||
|
ViewerToolbarRight,
|
||||||
|
} from '@pascal-app/editor'
|
||||||
|
import { Layers, Package, Settings } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
|
const SIDEBAR_TABS = [
|
||||||
{
|
{
|
||||||
id: 'site',
|
id: 'site',
|
||||||
label: 'Scene',
|
label: 'Scene',
|
||||||
component: () => null, // Built-in SitePanel handles this
|
component: () => null,
|
||||||
|
mobileDefaultSnap: 0.5,
|
||||||
|
mobileIcon: <Layers className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'items',
|
||||||
|
label: 'Items',
|
||||||
|
component: ItemsPanel,
|
||||||
|
mobileDefaultSnap: 0.5,
|
||||||
|
mobileIcon: <Package className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
label: 'Settings',
|
||||||
|
component: () => null,
|
||||||
|
mobileDefaultSnap: 0.5,
|
||||||
|
mobileIcon: <Settings className="h-5 w-5" />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+6
-27
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Environment variable validation for the editor app.
|
* Environment variable validation for the editor app.
|
||||||
*
|
*
|
||||||
* This file validates that required environment variables are set at runtime.
|
* This file validates environment variables used by the standalone editor app.
|
||||||
* Variables are defined in the root .env file.
|
* Values are loaded from the repo root .env.local by package scripts.
|
||||||
*
|
*
|
||||||
* @see https://env.t3.gg/docs/nextjs
|
* @see https://env.t3.gg/docs/nextjs
|
||||||
*/
|
*/
|
||||||
@@ -13,42 +13,21 @@ export const env = createEnv({
|
|||||||
/**
|
/**
|
||||||
* Server-side environment variables (not exposed to client)
|
* Server-side environment variables (not exposed to client)
|
||||||
*/
|
*/
|
||||||
server: {
|
server: {},
|
||||||
// Database
|
|
||||||
POSTGRES_URL: z.string().min(1),
|
|
||||||
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
BETTER_AUTH_SECRET: z.string().min(1),
|
|
||||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
||||||
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
||||||
|
|
||||||
// Email
|
|
||||||
RESEND_API_KEY: z.string().optional(),
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client-side environment variables (exposed to browser via NEXT_PUBLIC_)
|
* Client-side environment variables (exposed to browser via NEXT_PUBLIC_)
|
||||||
*/
|
*/
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
|
NEXT_PUBLIC_ASSETS_CDN_URL: z.string().optional(),
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().optional(),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime values - pulls from process.env
|
* Runtime values - pulls from process.env
|
||||||
*/
|
*/
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
// Server
|
NEXT_PUBLIC_ASSETS_CDN_URL:
|
||||||
POSTGRES_URL: process.env.POSTGRES_URL,
|
process.env.NEXT_PUBLIC_ASSETS_CDN_URL ?? process.env.NEXT_PUBLIC_EDITOR_ASSETS_CDN_URL,
|
||||||
SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
||||||
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
|
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
|
||||||
// Client
|
|
||||||
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { NextConfig } from 'next'
|
import type { NextConfig } from 'next'
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
logging: {
|
||||||
|
browserToTerminal: true,
|
||||||
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "dotenv -e ./.env.local --override -- next dev --port 3002",
|
"dev": "dotenv -e ../../.env.local -- next dev --port 3002",
|
||||||
"build": "dotenv -e ./.env.local --override -- next build",
|
"build": "dotenv -e ../../.env.local -- next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome lint",
|
"lint": "biome lint",
|
||||||
"check-types": "next typegen && tsc --noEmit"
|
"check-types": "next typegen && tsc --noEmit"
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"geist": "^1.7.0",
|
"geist": "^1.7.0",
|
||||||
|
"lucide-react": "^1.7.0",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -37,9 +38,9 @@
|
|||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"agentation": "^2.3.2",
|
"agentation": "^2.3.2",
|
||||||
"react-grab": "^0.1.25",
|
"react-grab": "^0.1.29",
|
||||||
"react-scan": "^0.5.3",
|
"react-scan": "^0.5.3",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "5.9.3"
|
"typescript": "6.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="4"/>
|
||||||
|
<path d="M12 2v2M12 20v2M2 12h2M20 12h2"/>
|
||||||
|
<path d="M12 8v1M12 15v1M8 12h1M15 12h1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 290 B |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"buildCommand": "cd ../.. && npx -y bun@1.3.13 run build --filter=editor",
|
||||||
|
"installCommand": "cd ../.. && npx -y bun@1.3.13 install --frozen-lockfile",
|
||||||
|
"outputDirectory": ".next",
|
||||||
|
"cleanUrls": true,
|
||||||
|
"trailingSlash": false,
|
||||||
|
"bunVersion": "1.x"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 0,
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "editor",
|
"name": "editor",
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"@biomejs/biome": "^2.4.6",
|
"@biomejs/biome": "^2.4.6",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"turbo": "^2.8.15",
|
"turbo": "^2.8.15",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.2",
|
||||||
"ultracite": "^7.2.5",
|
"ultracite": "^7.2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"geist": "^1.7.0",
|
"geist": "^1.7.0",
|
||||||
|
"lucide-react": "^1.7.0",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -42,15 +43,15 @@
|
|||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"agentation": "^2.3.2",
|
"agentation": "^2.3.2",
|
||||||
"react-grab": "^0.1.25",
|
"react-grab": "^0.1.29",
|
||||||
"react-scan": "^0.5.3",
|
"react-scan": "^0.5.3",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@pascal-app/core",
|
"name": "@pascal-app/core",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dedent": "^1.7.1",
|
"dedent": "^1.7.1",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
@@ -62,9 +63,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal/typescript-config": "*",
|
"@pascal/typescript-config": "*",
|
||||||
|
"@types/bun": "^1.3.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/three": "^0.184.0",
|
"@types/three": "^0.184.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
},
|
},
|
||||||
"packages/editor": {
|
"packages/editor": {
|
||||||
"name": "@pascal-app/editor",
|
"name": "@pascal-app/editor",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -100,29 +102,29 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^1.7.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"motion": "^12.34.3",
|
"motion": "^12.34.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"three-mesh-bvh": "^0.9.8",
|
"three-mesh-bvh": "~0.9.8",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.11",
|
"zustand": "^5.0.11",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal-app/core": "^0.6.0",
|
"@pascal-app/core": "^0.7.0",
|
||||||
"@pascal-app/viewer": "^0.6.0",
|
"@pascal-app/viewer": "^0.7.0",
|
||||||
"@pascal/typescript-config": "*",
|
"@pascal/typescript-config": "*",
|
||||||
|
"@types/bun": "^1.3.0",
|
||||||
"@types/howler": "^2.2.12",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/node": "^22.19.12",
|
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@types/three": "^0.184.0",
|
"@types/three": "^0.184.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@pascal-app/core": "^0.6.0",
|
"@pascal-app/core": "^0.7.0",
|
||||||
"@pascal-app/viewer": "^0.6.0",
|
"@pascal-app/viewer": "^0.7.0",
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
"@react-three/fiber": "^9",
|
"@react-three/fiber": "^9",
|
||||||
"next": ">=15",
|
"next": ">=15",
|
||||||
@@ -159,13 +161,13 @@
|
|||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal-app/core": "workspace:*",
|
"@pascal-app/core": "^0.7.0",
|
||||||
"@pascal/typescript-config": "*",
|
"@pascal/typescript-config": "*",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@pascal-app/core": "workspace:*",
|
"@pascal-app/core": "^0.7.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/typescript-config": {
|
"packages/typescript-config": {
|
||||||
@@ -191,7 +193,7 @@
|
|||||||
},
|
},
|
||||||
"packages/viewer": {
|
"packages/viewer": {
|
||||||
"name": "@pascal-app/viewer",
|
"name": "@pascal-app/viewer",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"polygon-clipping": "^0.15.7",
|
"polygon-clipping": "^0.15.7",
|
||||||
"three-bvh-csg": "^0.0.18",
|
"three-bvh-csg": "^0.0.18",
|
||||||
@@ -200,13 +202,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal/typescript-config": "*",
|
"@pascal/typescript-config": "*",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/three": "^0.184.0",
|
"@types/three": "^0.184.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@pascal-app/core": "^0.6.0",
|
"@pascal-app/core": "^0.7.0",
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
"@react-three/fiber": "^9",
|
"@react-three/fiber": "^9",
|
||||||
"react": "^18 || ^19",
|
"react": "^18 || ^19",
|
||||||
@@ -218,14 +220,19 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@types/three": "0.184.0",
|
||||||
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
"@antfu/ni": ["@antfu/ni@0.23.2", "", { "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nu": "bin/nu.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs" } }, "sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ=="],
|
"@antfu/ni": ["@antfu/ni@30.1.0", "", { "dependencies": { "fzf": "^0.5.2", "package-manager-detector": "^1.6.0", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15" }, "bin": { "ni": "bin/ni.mjs", "nci": "bin/nci.mjs", "nr": "bin/nr.mjs", "nup": "bin/nup.mjs", "nd": "bin/nd.mjs", "nlx": "bin/nlx.mjs", "na": "bin/na.mjs", "nun": "bin/nun.mjs" } }, "sha512-3VuAbPjgY52rQNn4wABaXMhBU2Oq91uy6L8nX49eJ35OLI68CyckGU+HZxcaHix4ymuGM2nFL1D6sLpgODK5xw=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
@@ -247,7 +254,7 @@
|
|||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
|
|
||||||
@@ -257,27 +264,27 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.4.9", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.9", "@biomejs/cli-darwin-x64": "2.4.9", "@biomejs/cli-linux-arm64": "2.4.9", "@biomejs/cli-linux-arm64-musl": "2.4.9", "@biomejs/cli-linux-x64": "2.4.9", "@biomejs/cli-linux-x64-musl": "2.4.9", "@biomejs/cli-win32-arm64": "2.4.9", "@biomejs/cli-win32-x64": "2.4.9" }, "bin": { "biome": "bin/biome" } }, "sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA=="],
|
"@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA=="],
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg=="],
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw=="],
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ=="],
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.9", "", { "os": "linux", "cpu": "x64" }, "sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A=="],
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g=="],
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.9", "", { "os": "win32", "cpu": "x64" }, "sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="],
|
||||||
|
|
||||||
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
|
"@clack/core": ["@clack/core@1.3.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA=="],
|
||||||
|
|
||||||
"@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="],
|
"@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="],
|
||||||
|
|
||||||
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
||||||
|
|
||||||
@@ -289,59 +296,7 @@
|
|||||||
|
|
||||||
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
|
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
@@ -371,9 +326,11 @@
|
|||||||
|
|
||||||
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||||
|
|
||||||
|
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||||
|
|
||||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
@@ -445,15 +402,13 @@
|
|||||||
|
|
||||||
"@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="],
|
"@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="],
|
||||||
|
|
||||||
"@medv/finder": ["@medv/finder@4.0.2", "", {}, "sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ=="],
|
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
"@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="],
|
"@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="],
|
"@next/env": ["@next/env@16.2.1", "", {}, "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.14", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-ogBjgsFrPPz19abP3VwcYSahbkUOMMvJjxCOYWYndw+PydeMuLuB4XrvNkNutFrTjC9St2KFULRdKID8Sd/CMQ=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.18", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-w4MYq8M26a8PNrfto0JosLf5/3ssln1rsyP96g2DkC8uFVymStM5DLSz5ElxxrPRg2XnTMnFo3kREFlhYvxhWw=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q=="],
|
||||||
|
|
||||||
@@ -489,19 +444,19 @@
|
|||||||
|
|
||||||
"@pascal/typescript-config": ["@pascal/typescript-config@workspace:tooling/typescript"],
|
"@pascal/typescript-config": ["@pascal/typescript-config@workspace:tooling/typescript"],
|
||||||
|
|
||||||
"@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.64", "", {}, "sha512-fyDNvCIGUTUYxDOzQnLOAGEuDQbyjx1JUlEYrjHB8KLDWzVl0j2SoFiLN4AwahEh4pXiA7XdJBNneQKnfMa/Bw=="],
|
"@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.67", "", {}, "sha512-tBEK+suALkhZ5i9rJBR0PYx4uvDD0nVQGoEENBMLTmeL1kNzhbJ7+mfAv1ATW1w42TOuREI9FfAa5ca1N8pcxQ=="],
|
||||||
|
|
||||||
"@pmndrs/pointer-events": ["@pmndrs/pointer-events@6.6.29", "", {}, "sha512-o4YD6VfJgDYjFgde/YyAw2X5KY454tdmOXrHGOvKTWJBHzkL90B5vH4rqmexwRVvaDfT3YLvVh/Dm5cBbgZXMg=="],
|
"@pmndrs/pointer-events": ["@pmndrs/pointer-events@6.6.29", "", {}, "sha512-o4YD6VfJgDYjFgde/YyAw2X5KY454tdmOXrHGOvKTWJBHzkL90B5vH4rqmexwRVvaDfT3YLvVh/Dm5cBbgZXMg=="],
|
||||||
|
|
||||||
"@pmndrs/uikit": ["@pmndrs/uikit@1.0.64", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.64", "@pmndrs/uikit-pub-sub": "^1.0.64", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-FQgF8sf46U6fmHnqOn32S04lnWiXVxQO6MatL7FgoLKJ74YIBgEHgwKHRwltgLrN/rcTu7LgJm7LgvXvMccEGA=="],
|
"@pmndrs/uikit": ["@pmndrs/uikit@1.0.67", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.67", "@pmndrs/uikit-pub-sub": "^1.0.67", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-36zaHVHxRPSWgr78r1GUEP0wcfC2hvx1a5t459Yx9IOYqolKCCloCticoSDCQHNnEbeC53xSDMbb70dz8IIljA=="],
|
||||||
|
|
||||||
"@pmndrs/uikit-lucide": ["@pmndrs/uikit-lucide@1.0.64", "", { "dependencies": { "@pmndrs/uikit": "^1.0.64" } }, "sha512-WU9CqboaBJHLvAk3Qt6V+7oMSk2lafY/ZwdcjuwSt3U+ZExHMnM24APeBdwi6w0e0sER2I/qiSVDs26JPOg7ew=="],
|
"@pmndrs/uikit-lucide": ["@pmndrs/uikit-lucide@1.0.67", "", { "dependencies": { "@pmndrs/uikit": "^1.0.67" } }, "sha512-/qJlNX/xd+KsbS2SdU/7VBGGk67VS4tx2X/2A6FeMa2L2/NsvLjs7mReoRLR2DvKVHsz6GxPJd0m3uSoRjuELw=="],
|
||||||
|
|
||||||
"@pmndrs/uikit-pub-sub": ["@pmndrs/uikit-pub-sub@1.0.64", "", { "dependencies": { "@preact/signals-core": "^1.8.0" } }, "sha512-+hlc5aWC3wQlA/zihwh+OGSm3aY9O+L/TEuNLwXw6R8syHMv7obqeY6s/hcjzD92k6DRQZLhVzVy+yqqVKAbRA=="],
|
"@pmndrs/uikit-pub-sub": ["@pmndrs/uikit-pub-sub@1.0.67", "", { "dependencies": { "@preact/signals-core": "^1.8.0" } }, "sha512-WC14YjujQ+gIliaEN06/7zygIwxKzweasNicwqVjTQ1PRJLe/HK9pyyS7DBAtfUy9y/9i5dfVFJLhGI8R61dWA=="],
|
||||||
|
|
||||||
"@preact/signals": ["@preact/signals@1.3.4", "", { "dependencies": { "@preact/signals-core": "^1.7.0" }, "peerDependencies": { "preact": "10.x" } }, "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g=="],
|
"@preact/signals": ["@preact/signals@1.3.4", "", { "dependencies": { "@preact/signals-core": "^1.7.0" }, "peerDependencies": { "preact": "10.x" } }, "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g=="],
|
||||||
|
|
||||||
"@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="],
|
"@preact/signals-core": ["@preact/signals-core@1.14.1", "", {}, "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng=="],
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
@@ -581,15 +536,15 @@
|
|||||||
|
|
||||||
"@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.29", "", { "dependencies": { "@antfu/ni": "^0.23.0", "commander": "^14.0.0", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^8.2.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.0" }, "bin": { "react-grab": "dist/cli.js" } }, "sha512-idgTec/ganPsag5BvbR3srF42UNNoUwHY+p94YORk5sCBl9ZaGJOXcDO93F8M4wruzTqIZgf1MUeY0+sTo0fVA=="],
|
"@react-grab/cli": ["@react-grab/cli@0.1.33", "", { "dependencies": { "@antfu/ni": "^30.1.0", "commander": "^14.0.3", "ignore": "^7.0.5", "jsonc-parser": "^3.3.1", "ora": "^9.4.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "smol-toml": "^1.6.1" }, "bin": { "react-grab": "bin/cli.js" } }, "sha512-UOc3PwN11Osw0NzaxRLK8trP4X+5iW1Dst3gvHRCafe3wXHyadzHYH8H1hdkcXdlIx3gsoD9ASJ+G/JH+A/jqA=="],
|
||||||
|
|
||||||
"@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="],
|
"@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="],
|
||||||
|
|
||||||
"@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="],
|
"@react-three/fiber": ["@react-three/fiber@9.6.1", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg=="],
|
||||||
|
|
||||||
"@react-three/uikit": ["@react-three/uikit@1.0.64", "", { "dependencies": { "@pmndrs/pointer-events": "^6.6.29", "@pmndrs/uikit": "^1.0.64", "@preact/signals-core": "^1.5.1", "suspend-react": "^0.1.3", "zustand": "^5.0.6" }, "peerDependencies": { "@react-three/fiber": ">=8", "react": ">=18" } }, "sha512-//wUWePJHHHWgDokWs2WUs8yUqOJ1Dy/g1OufGHw/BeTujtoNG0hmuISjdeDXZOQPfNjpMgrbemfmvryTeCfLg=="],
|
"@react-three/uikit": ["@react-three/uikit@1.0.67", "", { "dependencies": { "@pmndrs/pointer-events": "^6.6.29", "@pmndrs/uikit": "^1.0.67", "@preact/signals-core": "^1.5.1", "suspend-react": "^0.1.3", "zustand": "^5.0.6" }, "peerDependencies": { "@react-three/fiber": ">=8", "react": ">=18" } }, "sha512-yq0dL8S0NDY2ffudRtnEGvrntRAJPwMGKWnJFZdbulP1AXgTwpiSV4ES6zlwjiXL4or3FiOeV0Vz3T94SGUSMw=="],
|
||||||
|
|
||||||
"@react-three/uikit-lucide": ["@react-three/uikit-lucide@1.0.64", "", { "dependencies": { "@pmndrs/uikit-lucide": "^1.0.64", "@react-three/uikit": "^1.0.64" } }, "sha512-7csHVPDa6OoKog3HrRo4E9OuC4n98Fj9iDsZnNbNUt0YjocoUCBygCStSqaFcZnppgy78eMk1qS6nfu78R0AZw=="],
|
"@react-three/uikit-lucide": ["@react-three/uikit-lucide@1.0.67", "", { "dependencies": { "@pmndrs/uikit-lucide": "^1.0.67", "@react-three/uikit": "^1.0.67" } }, "sha512-MsJYwLFibaX3aa8FFiJPz1Vm9tIAXyrWxIdkiaigKPlaM3yIo7DkTY1E5Xkq10X+EP7f48UjNDa1cbX0eqXo6g=="],
|
||||||
|
|
||||||
"@repo/eslint-config": ["@repo/eslint-config@workspace:packages/eslint-config"],
|
"@repo/eslint-config": ["@repo/eslint-config@workspace:packages/eslint-config"],
|
||||||
|
|
||||||
@@ -601,65 +556,67 @@
|
|||||||
|
|
||||||
"@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.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
|
||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="],
|
||||||
|
|
||||||
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="],
|
"@turbo/darwin-64": ["@turbo/darwin-64@2.9.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-eu3eFRmE9NjgZ0wPdRJ44l+LGSeIky+tz5ZQd8zQkw/Yqi+BM7wq+8nbabeoiVUcICi/IZweMOKl/MCmkrd1+g=="],
|
||||||
|
|
||||||
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="],
|
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RUkAE404z/J8NsyrUosMcBaXT6M4bRFxTQrmkDQBLQVXaC8Jl0e9bMvYDSX0GW7Ffm2m3j9y7RXgR1foeUAM9w=="],
|
||||||
|
|
||||||
"@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="],
|
"@turbo/linux-64": ["@turbo/linux-64@2.9.12", "", { "os": "linux", "cpu": "x64" }, "sha512-InIUtH7cw/vqXNX1Gr7QgWfmw3ct08pV5CpfdEOR48z2u2rzdmpIuk00B/Q2xCb0PMWtKgiMQynfuphmEuUyTQ=="],
|
||||||
|
|
||||||
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="],
|
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-lC6nD//Xh67fmJM0LKaLsg74Wry0aYrgMklpiNgCbUaMdPIOqj0A00iri3NU7Lb7pZHx8ViisgpeDKlpSgFUCA=="],
|
||||||
|
|
||||||
"@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="],
|
"@turbo/windows-64": ["@turbo/windows-64@2.9.12", "", { "os": "win32", "cpu": "x64" }, "sha512-conYri8VUl72JOdYnLDPYwzqbPcY5ECoHmo9FWoKznemhaAIilj4maHqs9Uar0aKfNoZIULniy+6iWaLtLO34A=="],
|
||||||
|
|
||||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
|
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-XoR4bsg62/L/esRVcmoMESEiNZ36+YmyjYGLpoqk8nwMgXzzVjNOgX0lRSz5w/U/ajLGv3nhMsS0Q2QOdvp2AQ=="],
|
||||||
|
|
||||||
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||||
|
|
||||||
"@types/draco3d": ["@types/draco3d@1.4.10", "", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="],
|
"@types/draco3d": ["@types/draco3d@1.4.10", "", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|
||||||
"@types/howler": ["@types/howler@2.2.12", "", {}, "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g=="],
|
"@types/howler": ["@types/howler@2.2.12", "", {}, "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
"@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
|
||||||
|
|
||||||
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
|
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
|
"@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
|
||||||
|
|
||||||
@@ -669,25 +626,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.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="],
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="],
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="],
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="],
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
|
||||||
|
|
||||||
"@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
|
"@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="],
|
||||||
|
|
||||||
@@ -697,8 +654,6 @@
|
|||||||
|
|
||||||
"@visual-json/react": ["@visual-json/react@0.4.0", "", { "dependencies": { "@visual-json/core": "0.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-qiDrXI7wu9VoTE0TaLoJp7kwRj8SQTpkS/wkts5sBPA8078zdrIKc+0u0Y8wAXGYd3udw9dNfD4kmpO48OEwXQ=="],
|
"@visual-json/react": ["@visual-json/react@0.4.0", "", { "dependencies": { "@visual-json/core": "0.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-qiDrXI7wu9VoTE0TaLoJp7kwRj8SQTpkS/wkts5sBPA8078zdrIKc+0u0Y8wAXGYd3udw9dNfD4kmpO48OEwXQ=="],
|
||||||
|
|
||||||
"@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
|
||||||
|
|
||||||
"@zappar/msdf-generator": ["@zappar/msdf-generator@1.2.4", "", { "dependencies": { "comlink": "^4.4.2" } }, "sha512-6S/MCk0Ky0ipewZJw4xFEzH/2aYfWmPXEkTdBtNyDDfkbicrNwgJgtxZ4SnTDyNe9XHMqDA4sL9srRsgDLRMqA=="],
|
"@zappar/msdf-generator": ["@zappar/msdf-generator@1.2.4", "", { "dependencies": { "comlink": "^4.4.2" } }, "sha512-6S/MCk0Ky0ipewZJw4xFEzH/2aYfWmPXEkTdBtNyDDfkbicrNwgJgtxZ4SnTDyNe9XHMqDA4sL9srRsgDLRMqA=="],
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
@@ -709,7 +664,7 @@
|
|||||||
|
|
||||||
"agentation": ["agentation@2.3.3", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-AUZgFCdBQ/nAohlFsHByM9S2Dp7ECMNqVjlOke4hv/90v+wTiwrGladEkgWS60RDQp+CJ5p97meeCthYgTFlKQ=="],
|
"agentation": ["agentation@2.3.3", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-AUZgFCdBQ/nAohlFsHByM9S2Dp7ECMNqVjlOke4hv/90v+wTiwrGladEkgWS60RDQp+CJ5p97meeCthYgTFlKQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
@@ -743,25 +698,27 @@
|
|||||||
|
|
||||||
"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.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ=="],
|
||||||
|
|
||||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||||
|
|
||||||
"bippy": ["bippy@0.5.32", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-yt1mC8eReTxjfg41YBZdN4PvsDwHFWxltoiQX0Q+Htlbf41aSniopb7ECZits01HwNAvXEh69RGk/ImlswDTEw=="],
|
"bippy": ["bippy@0.5.39", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-8hE8rKSl8JWyeaY+JjpnmceWAZPpLEyzOZQpWXM5Rc7861c5WotMJHy2aRZKZrGA8nMpvLNF01t4yQQ+HcZG3w=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
@@ -771,17 +728,17 @@
|
|||||||
|
|
||||||
"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.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
|
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||||
|
|
||||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||||
|
|
||||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
@@ -845,7 +802,7 @@
|
|||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||||
|
|
||||||
"dotenv-cli": ["dotenv-cli@11.0.0", "", { "dependencies": { "cross-spawn": "^7.0.6", "dotenv": "^17.1.0", "dotenv-expand": "^12.0.0", "minimist": "^1.2.6" }, "bin": { "dotenv": "cli.js" } }, "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww=="],
|
"dotenv-cli": ["dotenv-cli@11.0.0", "", { "dependencies": { "cross-spawn": "^7.0.6", "dotenv": "^17.1.0", "dotenv-expand": "^12.0.0", "minimist": "^1.2.6" }, "bin": { "dotenv": "cli.js" } }, "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww=="],
|
||||||
|
|
||||||
@@ -859,23 +816,19 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="],
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
"enhanced-resolve": ["enhanced-resolve@5.21.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ=="],
|
||||||
|
|
||||||
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
"es-iterator-helpers": ["es-iterator-helpers@1.3.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" } }, "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ=="],
|
"es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="],
|
||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
@@ -885,8 +838,6 @@
|
|||||||
|
|
||||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
@@ -903,7 +854,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.20", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-sMDremg73XbX+Imh7iDRS3TAro5jIf7CdzSqrT50wCGhClbiUvmWcLomnX+ldUbfTy+OvtqOeePvTivJMR87eQ=="],
|
"eslint-plugin-turbo": ["eslint-plugin-turbo@2.9.12", "", { "dependencies": { "dotenv": "16.0.3" }, "peerDependencies": { "eslint": ">6.6.0", "turbo": ">2.0.0" } }, "sha512-Ilv1DTYyghdIdTUsW/VbjVTYKt1Hfs7X2C+rq/i4u7ZVofzcHZwk/3QwrV5UyOGADmxmWNyD+xcRS7+ZE5arQg=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
@@ -919,7 +870,7 @@
|
|||||||
|
|
||||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
@@ -927,11 +878,11 @@
|
|||||||
|
|
||||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||||
|
|
||||||
"eventsource-parser": ["eventsource-parser@3.0.7", "", {}, "sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA=="],
|
"eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||||
|
|
||||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
"express-rate-limit": ["express-rate-limit@8.5.1", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
@@ -941,7 +892,13 @@
|
|||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||||
|
|
||||||
|
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||||
|
|
||||||
|
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
@@ -975,13 +932,15 @@
|
|||||||
|
|
||||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||||
|
|
||||||
|
"fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="],
|
||||||
|
|
||||||
"geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="],
|
"geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="],
|
||||||
|
|
||||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||||
|
|
||||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
@@ -1017,11 +976,11 @@
|
|||||||
|
|
||||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||||
|
|
||||||
"hls.js": ["hls.js@1.6.15", "", {}, "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA=="],
|
"hls.js": ["hls.js@1.6.16", "", {}, "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA=="],
|
||||||
|
|
||||||
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
|
"hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="],
|
||||||
|
|
||||||
"howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="],
|
"howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="],
|
||||||
|
|
||||||
@@ -1045,7 +1004,7 @@
|
|||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
@@ -1059,7 +1018,7 @@
|
|||||||
|
|
||||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||||
|
|
||||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||||
|
|
||||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||||
|
|
||||||
@@ -1113,9 +1072,9 @@
|
|||||||
|
|
||||||
"its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="],
|
"its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||||
|
|
||||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
@@ -1173,13 +1132,13 @@
|
|||||||
|
|
||||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
|
"lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
"lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="],
|
||||||
|
|
||||||
"maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
|
"maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
|
||||||
|
|
||||||
@@ -1221,7 +1180,7 @@
|
|||||||
|
|
||||||
"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.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
|
"nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
|
||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
@@ -1231,11 +1190,11 @@
|
|||||||
|
|
||||||
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
"node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="],
|
||||||
|
|
||||||
"number-flow": ["number-flow@0.5.12", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-CIs21h2JkfYG4rfgERaUNAk0Cz+Ef14fNJfSCbGGhgRgconQc9b7rcCQfi9SZ36kNjVXmsl2BrzDbjGtEgumAA=="],
|
"number-flow": ["number-flow@0.5.12", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-CIs21h2JkfYG4rfgERaUNAk0Cz+Ef14fNJfSCbGGhgRgconQc9b7rcCQfi9SZ36kNjVXmsl2BrzDbjGtEgumAA=="],
|
||||||
|
|
||||||
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
|
"nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
@@ -1259,7 +1218,7 @@
|
|||||||
|
|
||||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
|
"ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="],
|
||||||
|
|
||||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||||
|
|
||||||
@@ -1267,6 +1226,8 @@
|
|||||||
|
|
||||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
|
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||||
|
|
||||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
@@ -1293,11 +1254,11 @@
|
|||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||||
|
|
||||||
"potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
|
"potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
|
||||||
|
|
||||||
"preact": ["preact@10.29.0", "", {}, "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg=="],
|
"preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="],
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
@@ -1319,11 +1280,11 @@
|
|||||||
|
|
||||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||||
|
|
||||||
"react-grab": ["react-grab@0.1.29", "", { "dependencies": { "@medv/finder": "^4.0.2", "@react-grab/cli": "0.1.29", "bippy": "^0.5.32", "element-source": "^0.0.3", "solid-js": "^1.9.10" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-s6CDIwjA0pzhCJqgFabM3GZ6m9vsmStOHopl7c8Xyx702QZ5ZXxOMRTpd2/7Gg+GC/1XwFmyLHwQiOCbUYIIYQ=="],
|
"react-grab": ["react-grab@0.1.33", "", { "dependencies": { "@react-grab/cli": "0.1.33", "bippy": "^0.5.39" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-ER919JMsE4TTrb2CpEivqsIjNMSycD4HtS8v7mS3pq67U7WL1K3+C8m9AYOwW4dpuYh+EanC2eJBmfuczHJZ0A=="],
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
@@ -1331,7 +1292,7 @@
|
|||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
"react-scan": ["react-scan@0.5.3", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/generator": "^7.26.2", "@babel/types": "^7.26.0", "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "@types/node": "^20.17.9", "bippy": "^0.5.30", "commander": "^14.0.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "picocolors": "^1.1.1", "preact": "^10.25.1", "prompts": "^2.4.2" }, "optionalDependencies": { "unplugin": "2.1.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "bin": { "react-scan": "bin/cli.js" } }, "sha512-qde9PupmUf0L3MU1H6bjmoukZNbCXdMyTEwP4Gh8RQ4rZPd2GGNBgEKWszwLm96E8k+sGtMpc0B9P0KyFDP6Bw=="],
|
"react-scan": ["react-scan@0.5.6", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/types": "^7.26.0", "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "bippy": "^0.5.39", "commander": "^14.0.0", "picocolors": "^1.1.1", "preact": "^10.25.1", "prompts": "^2.4.2", "react-grab": "latest" }, "optionalDependencies": { "unplugin": "2.1.0" }, "peerDependencies": { "esbuild": ">=0.18.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["esbuild"], "bin": { "react-scan": "bin/cli.js" } }, "sha512-FrZ75yWjabNQmBbN9wxP+FgfgcPYoOFFWXYBhi6OnUbF/DyJDwOEfV2RRs9IWVfPVjhsku2CPK3oohYC5GEHpg=="],
|
||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
@@ -1357,7 +1318,7 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
"safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
|
||||||
|
|
||||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||||
|
|
||||||
@@ -1371,10 +1332,6 @@
|
|||||||
|
|
||||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
|
|
||||||
"seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="],
|
|
||||||
|
|
||||||
"seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="],
|
|
||||||
|
|
||||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||||
|
|
||||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
@@ -1393,7 +1350,7 @@
|
|||||||
|
|
||||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||||
|
|
||||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
@@ -1405,8 +1362,6 @@
|
|||||||
|
|
||||||
"smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
"smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
||||||
|
|
||||||
"solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
|
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"splaytree": ["splaytree@3.2.3", "", {}, "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w=="],
|
"splaytree": ["splaytree@3.2.3", "", {}, "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w=="],
|
||||||
@@ -1417,11 +1372,11 @@
|
|||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
|
||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
"string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
||||||
|
|
||||||
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||||
|
|
||||||
@@ -1447,9 +1402,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.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
"tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
"three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="],
|
"three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="],
|
||||||
|
|
||||||
@@ -1459,9 +1414,9 @@
|
|||||||
|
|
||||||
"three-stdlib": ["three-stdlib@2.36.1", "", { "dependencies": { "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", "@types/webxr": "^0.5.2", "draco3d": "^1.4.1", "fflate": "^0.6.9", "potpack": "^1.0.1" }, "peerDependencies": { "three": ">=0.128.0" } }, "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg=="],
|
"three-stdlib": ["three-stdlib@2.36.1", "", { "dependencies": { "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", "@types/webxr": "^0.5.2", "draco3d": "^1.4.1", "fflate": "^0.6.9", "potpack": "^1.0.1" }, "peerDependencies": { "three": ">=0.128.0" } }, "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
"tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
@@ -1479,7 +1434,7 @@
|
|||||||
|
|
||||||
"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.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="],
|
"turbo": ["turbo@2.9.12", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.12", "@turbo/darwin-arm64": "2.9.12", "@turbo/linux-64": "2.9.12", "@turbo/linux-arm64": "2.9.12", "@turbo/windows-64": "2.9.12", "@turbo/windows-arm64": "2.9.12" }, "bin": { "turbo": "bin/turbo" } }, "sha512-lCPgus1NuTiBdaITWqzSH/Ff6HVL8HHGBtOXHg1dHRfcshN79XkygSdh0M6g8b0td91ILLG5MTkLOkp5UvyPJw=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
@@ -1495,15 +1450,15 @@
|
|||||||
|
|
||||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="],
|
"typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
|
||||||
|
|
||||||
"ultracite": ["ultracite@7.3.2", "", { "dependencies": { "@clack/prompts": "^1.1.0", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-JV2TdeBDc1aaj3wFzXLjRcBlUZkXBdShMLAjHh9YLK0rXPSK42CzHqfWXUfibFUAUiUGNwoOFnIpS45EaJAcdg=="],
|
"ultracite": ["ultracite@7.6.5", "", { "dependencies": { "@clack/prompts": "^1.3.0", "commander": "^14.0.3", "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.6", "yaml": "^2.8.4", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.0.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-q68Ht5cTvjzmyDvhwZUPfxWLpcwioG5e5ODrVxBAbc0qWEn5hD3+CT1Tgq8FEFEypjKVote01inkG1nDOE9hmw=="],
|
||||||
|
|
||||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||||
|
|
||||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
@@ -1545,31 +1500,33 @@
|
|||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||||
|
|
||||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
||||||
"zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="],
|
"zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="],
|
||||||
|
|
||||||
"zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
|
"zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
"@pascal-app/editor/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
"@pascal-app/mcp/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"@pascal-app/mcp/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
"@pascal-app/viewer/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="],
|
||||||
|
|
||||||
"@pascal-app/viewer/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
@@ -1593,27 +1550,29 @@
|
|||||||
|
|
||||||
"@react-three/drei/three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="],
|
"@react-three/drei/three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="],
|
||||||
|
|
||||||
"@repo/ui/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
"@repo/eslint-config/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@repo/ui/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
"@repo/ui/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
@@ -1621,19 +1580,15 @@
|
|||||||
|
|
||||||
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
"editor/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
"editor/@types/node": ["@types/node@22.19.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ=="],
|
||||||
|
|
||||||
"eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
"eslint-plugin-turbo/dotenv": ["dotenv@16.0.3", "", {}, "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="],
|
"eslint-plugin-turbo/dotenv": ["dotenv@16.0.3", "", {}, "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
"glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
|
||||||
|
|
||||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
@@ -1641,15 +1596,11 @@
|
|||||||
|
|
||||||
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||||
|
|
||||||
"react-scan/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="],
|
|
||||||
|
|
||||||
"router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
"router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"sharp/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
|
||||||
|
|
||||||
"stats-gl/@types/three": ["@types/three@0.183.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~1.0.1" } }, "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw=="],
|
|
||||||
|
|
||||||
"stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
|
"stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
|
||||||
|
|
||||||
@@ -1659,19 +1610,19 @@
|
|||||||
|
|
||||||
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"@pascal-app/mcp/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"@pascal-app/viewer/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"@pascal-app/viewer/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"@repo/ui/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||||
|
|
||||||
|
"editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||||
|
|
||||||
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"next/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||||
|
|
||||||
"stats-gl/@types/three/meshoptimizer": ["meshoptimizer@1.0.1", "", {}, "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -12,23 +12,30 @@
|
|||||||
"check:fix": "biome check --write",
|
"check:fix": "biome check --write",
|
||||||
"check-types": "turbo run check-types",
|
"check-types": "turbo run check-types",
|
||||||
"kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'",
|
"kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'",
|
||||||
"release": "gh workflow run release.yml -f package=both -f bump=patch",
|
"release": "gh workflow run release.yml -f package=all -f bump=patch",
|
||||||
"release:viewer": "gh workflow run release.yml -f package=viewer -f bump=patch",
|
"release:viewer": "gh workflow run release.yml -f package=viewer -f bump=patch",
|
||||||
"release:core": "gh workflow run release.yml -f package=core -f bump=patch",
|
"release:core": "gh workflow run release.yml -f package=core -f bump=patch",
|
||||||
"release:minor": "gh workflow run release.yml -f package=both -f bump=minor",
|
"release:editor": "gh workflow run release.yml -f package=editor -f bump=patch",
|
||||||
"release:major": "gh workflow run release.yml -f package=both -f bump=major"
|
"release:mcp": "gh workflow run release.yml -f package=mcp -f bump=patch",
|
||||||
|
"release:minor": "gh workflow run release.yml -f package=all -f bump=minor",
|
||||||
|
"release:major": "gh workflow run release.yml -f package=all -f bump=major"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.6",
|
"@biomejs/biome": "^2.4.6",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"turbo": "^2.8.15",
|
"turbo": "^2.8.15",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.2",
|
||||||
"ultracite": "^7.2.5"
|
"ultracite": "^7.2.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.3.0",
|
"packageManager": "bun@1.3.0",
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@types/three": "0.184.0"
|
||||||
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|||||||
@@ -73,9 +73,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal/typescript-config": "*",
|
"@pascal/typescript-config": "*",
|
||||||
|
"@types/bun": "^1.3.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"typescript": "5.9.3",
|
"@types/three": "^0.184.0",
|
||||||
"@types/three": "^0.184.0"
|
"typescript": "6.0.2"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"3d",
|
"3d",
|
||||||
|
|||||||
@@ -28,12 +28,13 @@ export interface GridEvent {
|
|||||||
/** World-space intersection point on the grid plane. */
|
/** World-space intersection point on the grid plane. */
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
/**
|
/**
|
||||||
* Building-local intersection point, relative to the currently selected building.
|
* Building-local intersection point — relative to the currently selected building.
|
||||||
* Equals `position` when no building is selected.
|
* Equals `position` when no building is selected.
|
||||||
* Use this for placing or committing anything that lives inside a building
|
* Use this for placing/committing anything that lives inside a building (walls, slabs, items, etc.).
|
||||||
* (walls, slabs, items, etc.).
|
|
||||||
*/
|
*/
|
||||||
localPosition: [number, number, number]
|
localPosition: [number, number, number]
|
||||||
|
faceIndex?: number
|
||||||
|
object: Object3D
|
||||||
nativeEvent: ThreeEvent<PointerEvent>
|
nativeEvent: ThreeEvent<PointerEvent>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export type StairSegmentEvent = NodeEvent<StairSegmentNode>
|
|||||||
export type WindowEvent = NodeEvent<WindowNode>
|
export type WindowEvent = NodeEvent<WindowNode>
|
||||||
export type DoorEvent = NodeEvent<DoorNode>
|
export type DoorEvent = NodeEvent<DoorNode>
|
||||||
|
|
||||||
// Event suffixes, exported for use in hooks
|
// Event suffixes - exported for use in hooks
|
||||||
export const eventSuffixes = [
|
export const eventSuffixes = [
|
||||||
'click',
|
'click',
|
||||||
'move',
|
'move',
|
||||||
@@ -99,7 +100,7 @@ export interface ThumbnailGenerateEvent {
|
|||||||
/**
|
/**
|
||||||
* When true, snap levels to their true positions before capturing (for a
|
* When true, snap levels to their true positions before capturing (for a
|
||||||
* consistent auto-thumbnail angle) and defer the capture if the tab is
|
* consistent auto-thumbnail angle) and defer the capture if the tab is
|
||||||
* hidden, the background auto-save path. Omit for user-driven captures
|
* hidden — the background auto-save path. Omit for user-driven captures
|
||||||
* that should fire immediately from the current camera pose.
|
* that should fire immediately from the current camera pose.
|
||||||
*/
|
*/
|
||||||
snapLevels?: boolean
|
snapLevels?: boolean
|
||||||
@@ -107,8 +108,10 @@ export interface ThumbnailGenerateEvent {
|
|||||||
|
|
||||||
export interface CameraControlFitSceneEvent {
|
export interface CameraControlFitSceneEvent {
|
||||||
/**
|
/**
|
||||||
* XZ-plane axis-aligned bounds for camera framing. Omitted values let the
|
* XZ-plane axis-aligned bounds of the scene's geometry, computed from the
|
||||||
* listener choose its default framing pose.
|
* scene graph (see `@pascal-app/editor`'s `computeSceneBoundsXZ`). The
|
||||||
|
* viewer's camera-controls listener frames the camera onto this box.
|
||||||
|
* Omitted values fall back to the camera's default pose.
|
||||||
*/
|
*/
|
||||||
bounds?: {
|
bounds?: {
|
||||||
min: [number, number]
|
min: [number, number]
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ export function insetPolygonFromCentroid(
|
|||||||
return polygon.map(([x, z]) => [x, z] as [number, number])
|
return polygon.map(([x, z]) => [x, z] as [number, number])
|
||||||
}
|
}
|
||||||
|
|
||||||
const centroid = polygon.reduce(
|
const centroid = polygon.reduce((acc, [x, z]) => ({ x: acc.x + x, z: acc.z + z }), { x: 0, z: 0 })
|
||||||
(acc, [x, z]) => ({ x: acc.x + x, z: acc.z + z }),
|
|
||||||
{ x: 0, z: 0 },
|
|
||||||
)
|
|
||||||
centroid.x /= Math.max(polygon.length, 1)
|
centroid.x /= Math.max(polygon.length, 1)
|
||||||
centroid.z /= Math.max(polygon.length, 1)
|
centroid.z /= Math.max(polygon.length, 1)
|
||||||
|
|
||||||
@@ -70,7 +67,10 @@ function dedupePolygonPoints(
|
|||||||
return deduped
|
return deduped
|
||||||
}
|
}
|
||||||
|
|
||||||
function simplifyPolyline(points: Array<[number, number]>, tolerance: number): Array<[number, number]> {
|
function simplifyPolyline(
|
||||||
|
points: Array<[number, number]>,
|
||||||
|
tolerance: number,
|
||||||
|
): Array<[number, number]> {
|
||||||
if (points.length <= 2) {
|
if (points.length <= 2) {
|
||||||
return points.map(([x, z]) => [x, z] as [number, number])
|
return points.map(([x, z]) => [x, z] as [number, number])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
getClampedWallCurveOffset,
|
CeilingNode,
|
||||||
getWallCurveFrameAt,
|
type CeilingNode as CeilingNodeType,
|
||||||
isCurvedWall,
|
SlabNode,
|
||||||
} from '../systems/wall/wall-curve'
|
type SlabNode as SlabNodeType,
|
||||||
import { CeilingNode, SlabNode, type CeilingNode as CeilingNodeType, type SlabNode as SlabNodeType, type WallNode } from '../schema'
|
type WallNode,
|
||||||
|
} from '../schema'
|
||||||
import {
|
import {
|
||||||
getSceneHistoryPauseDepth,
|
getSceneHistoryPauseDepth,
|
||||||
pauseSceneHistory,
|
pauseSceneHistory,
|
||||||
resumeSceneHistory,
|
resumeSceneHistory,
|
||||||
} from '../store/history-control'
|
} from '../store/history-control'
|
||||||
|
import {
|
||||||
|
getClampedWallCurveOffset,
|
||||||
|
getWallCurveFrameAt,
|
||||||
|
isCurvedWall,
|
||||||
|
} from '../systems/wall/wall-curve'
|
||||||
import { simplifyClosedPolygon } from './polygon-geometry'
|
import { simplifyClosedPolygon } from './polygon-geometry'
|
||||||
|
|
||||||
type Point2D = { x: number; y: number }
|
type Point2D = { x: number; y: number }
|
||||||
@@ -147,10 +153,10 @@ function polygonCentroid(points: Point2D[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bboxOf(points: Point2D[]) {
|
function bboxOf(points: Point2D[]) {
|
||||||
let minX = Infinity
|
let minX = Number.POSITIVE_INFINITY
|
||||||
let minY = Infinity
|
let minY = Number.POSITIVE_INFINITY
|
||||||
let maxX = -Infinity
|
let maxX = Number.NEGATIVE_INFINITY
|
||||||
let maxY = -Infinity
|
let maxY = Number.NEGATIVE_INFINITY
|
||||||
|
|
||||||
for (const point of points) {
|
for (const point of points) {
|
||||||
minX = Math.min(minX, point.x)
|
minX = Math.min(minX, point.x)
|
||||||
@@ -216,7 +222,13 @@ function sampleWallPointsForRoomDetection(
|
|||||||
return [start, end]
|
return [start, end]
|
||||||
}
|
}
|
||||||
|
|
||||||
const subdivide = (t0: number, p0: Point2D, t1: number, p1: Point2D, depth: number): Point2D[] => {
|
const subdivide = (
|
||||||
|
t0: number,
|
||||||
|
p0: Point2D,
|
||||||
|
t1: number,
|
||||||
|
p1: Point2D,
|
||||||
|
depth: number,
|
||||||
|
): Point2D[] => {
|
||||||
const midT = (t0 + t1) / 2
|
const midT = (t0 + t1) / 2
|
||||||
const midPoint = getWallCurveFrameAt(wall, midT).point
|
const midPoint = getWallCurveFrameAt(wall, midT).point
|
||||||
const deviation = pointLineDistance(midPoint, p0, p1)
|
const deviation = pointLineDistance(midPoint, p0, p1)
|
||||||
@@ -361,7 +373,7 @@ function extractRoomPolygons(walls: WallNode[]): Point2D[][] {
|
|||||||
|
|
||||||
const signedArea = polygonArea(polygon)
|
const signedArea = polygonArea(polygon)
|
||||||
if (signedArea <= 0) continue
|
if (signedArea <= 0) continue
|
||||||
if (signedArea < 0.5 || signedArea > 10000) continue
|
if (signedArea < 0.5 || signedArea > 10_000) continue
|
||||||
|
|
||||||
const signature = polygonSignature(polygon)
|
const signature = polygonSignature(polygon)
|
||||||
if (faces.some((face) => polygonSignature(face) === signature)) continue
|
if (faces.some((face) => polygonSignature(face) === signature)) continue
|
||||||
@@ -442,10 +454,7 @@ function nextAutoRoomName(
|
|||||||
return `Room ${maxIndex + 1} ${suffix}`
|
return `Room ${maxIndex + 1} ${suffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameTuplePolygon(
|
function sameTuplePolygon(current: Array<[number, number]>, next: Array<[number, number]>) {
|
||||||
current: Array<[number, number]>,
|
|
||||||
next: Array<[number, number]>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
current.length === next.length &&
|
current.length === next.length &&
|
||||||
current.every((point, index) => point[0] === next[index]?.[0] && point[1] === next[index]?.[1])
|
current.every((point, index) => point[0] === next[index]?.[0] && point[1] === next[index]?.[1])
|
||||||
@@ -727,7 +736,9 @@ function syncAutoCeilingsForLevel(
|
|||||||
const polygon = updatesById.get(ceiling.id)
|
const polygon = updatesById.get(ceiling.id)
|
||||||
if (!polygon) return []
|
if (!polygon) return []
|
||||||
|
|
||||||
return sameTuplePolygon(ceiling.polygon, polygon) ? [] : [{ id: ceiling.id, data: { polygon } }]
|
return sameTuplePolygon(ceiling.polygon, polygon)
|
||||||
|
? []
|
||||||
|
: [{ id: ceiling.id, data: { polygon } }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const plannedCeilingsForNaming: Array<{ name?: string }> = [...existingCeilings]
|
const plannedCeilingsForNaming: Array<{ name?: string }> = [...existingCeilings]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type MaterialPresetPayload,
|
type MaterialPresetPayload,
|
||||||
|
type MaterialTarget,
|
||||||
|
MaterialTarget as MaterialTargetSchema,
|
||||||
} from './schema/material'
|
} from './schema/material'
|
||||||
|
|
||||||
export type MaterialCatalogItem = {
|
export type MaterialCatalogItem = {
|
||||||
@@ -12,6 +14,32 @@ export type MaterialCatalogItem = {
|
|||||||
preset: MaterialPresetPayload
|
preset: MaterialPresetPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WALL_TARGETS: MaterialTarget[] = [MaterialTargetSchema.enum.wall]
|
||||||
|
|
||||||
|
const SLAB_TARGETS: MaterialTarget[] = [MaterialTargetSchema.enum.slab]
|
||||||
|
|
||||||
|
const WALL_AND_SLAB_TARGETS: MaterialTarget[] = [
|
||||||
|
MaterialTargetSchema.enum.wall,
|
||||||
|
MaterialTargetSchema.enum.slab,
|
||||||
|
]
|
||||||
|
|
||||||
|
const STAIR_TARGETS: MaterialTarget[] = [
|
||||||
|
MaterialTargetSchema.enum.stair,
|
||||||
|
MaterialTargetSchema.enum['stair-segment'],
|
||||||
|
]
|
||||||
|
|
||||||
|
const STAIR_AND_FENCE_TARGETS: MaterialTarget[] = [
|
||||||
|
...STAIR_TARGETS,
|
||||||
|
MaterialTargetSchema.enum.fence,
|
||||||
|
]
|
||||||
|
|
||||||
|
const ROOF_TARGETS: MaterialTarget[] = [
|
||||||
|
MaterialTargetSchema.enum.roof,
|
||||||
|
MaterialTargetSchema.enum['roof-segment'],
|
||||||
|
]
|
||||||
|
|
||||||
|
const CEILING_TARGETS: MaterialTarget[] = [MaterialTargetSchema.enum.ceiling]
|
||||||
|
|
||||||
export const MATERIAL_CATEGORIES = [
|
export const MATERIAL_CATEGORIES = [
|
||||||
'wood',
|
'wood',
|
||||||
'wallpaper',
|
'wallpaper',
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// @ts-expect-error — bun:test is provided by the Bun runtime; core does not
|
|
||||||
// depend on @types/bun so the import type is unresolved at compile time.
|
|
||||||
// The tsconfig in packages/core still emits this file; the @ts-expect-error
|
|
||||||
// keeps the build green while letting `bun test` pick it up normally.
|
|
||||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
import { ALLOWED_ORIGINS_ENV, AssetUrl } from './asset-url'
|
import { ALLOWED_ORIGINS_ENV, AssetUrl } from './asset-url'
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export const FenceNode = BaseNode.extend({
|
|||||||
materialPreset: z.string().optional(),
|
materialPreset: z.string().optional(),
|
||||||
start: z.tuple([z.number(), z.number()]),
|
start: z.tuple([z.number(), z.number()]),
|
||||||
end: z.tuple([z.number(), z.number()]),
|
end: z.tuple([z.number(), z.number()]),
|
||||||
curveOffset: z.number().optional(),
|
|
||||||
height: z.number().default(1.8),
|
height: z.number().default(1.8),
|
||||||
thickness: z.number().default(0.08),
|
thickness: z.number().default(0.08),
|
||||||
|
curveOffset: z.number().optional(),
|
||||||
baseHeight: z.number().default(0.22),
|
baseHeight: z.number().default(0.22),
|
||||||
postSpacing: z.number().default(2),
|
postSpacing: z.number().default(2),
|
||||||
postSize: z.number().default(0.1),
|
postSize: z.number().default(0.1),
|
||||||
@@ -29,8 +29,8 @@ export const FenceNode = BaseNode.extend({
|
|||||||
dedent`
|
dedent`
|
||||||
Fence node - used to represent a fence segment in the building/site level coordinate system
|
Fence node - used to represent a fence segment in the building/site level coordinate system
|
||||||
- start/end: fence endpoints in level coordinate system
|
- start/end: fence endpoints in level coordinate system
|
||||||
- curveOffset: midpoint sagitta offset used to bend the fence into an arc
|
|
||||||
- height/thickness: overall fence dimensions in meters
|
- height/thickness: overall fence dimensions in meters
|
||||||
|
- curveOffset: midpoint sagitta offset used to bend the fence into an arc
|
||||||
- baseHeight/postSpacing/postSize/topRailHeight: exact geometric controls from the plan3D fence model
|
- baseHeight/postSpacing/postSize/topRailHeight: exact geometric controls from the plan3D fence model
|
||||||
- groundClearance/edgeInset/baseStyle: fence support and inset configuration
|
- groundClearance/edgeInset/baseStyle: fence support and inset configuration
|
||||||
- color/style: visual appearance options
|
- color/style: visual appearance options
|
||||||
|
|||||||
@@ -83,6 +83,18 @@ const assetSchema = z.object({
|
|||||||
// Optional top-down 2D image shown inside the item's footprint on the
|
// Optional top-down 2D image shown inside the item's footprint on the
|
||||||
// floor plan. When present, replaces the default diagonal-cross marker.
|
// floor plan. When present, replaces the default diagonal-cross marker.
|
||||||
floorPlanUrl: z.string().optional(),
|
floorPlanUrl: z.string().optional(),
|
||||||
|
// Where the item came from in the catalog. Used by the editor's items
|
||||||
|
// panel to filter Library / Community / Mine. The server populates it
|
||||||
|
// from `items.userId`: null → 'library', current user → 'mine',
|
||||||
|
// other user → 'community'. Defaults to 'library' when absent (e.g.
|
||||||
|
// the seeded built-in catalog).
|
||||||
|
source: z.enum(['library', 'community', 'mine']).default('library'),
|
||||||
|
// True when the item belongs to the caller and is still in draft status.
|
||||||
|
// The catalog only loads my drafts (other users' drafts are never
|
||||||
|
// published to the catalog). Used so the Community filter can include
|
||||||
|
// *my* published items alongside other users', while leaving drafts
|
||||||
|
// visible only under Mine.
|
||||||
|
isDraft: z.boolean().optional(),
|
||||||
src: AssetUrl,
|
src: AssetUrl,
|
||||||
dimensions: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]), // [w, h, d]
|
dimensions: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]), // [w, h, d]
|
||||||
attachTo: z.enum(['wall', 'wall-side', 'ceiling']).optional(),
|
attachTo: z.enum(['wall', 'wall-side', 'ceiling']).optional(),
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import dedent from 'dedent'
|
import dedent from 'dedent'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { BaseNode, nodeType, objectId } from '../base'
|
import { BaseNode, nodeType, objectId } from '../base'
|
||||||
import { type MaterialSchema, MaterialSchema as MaterialSchemaSchema } from '../material'
|
import type { MaterialSchema as MaterialSchemaType } from '../material'
|
||||||
|
import { MaterialSchema } from '../material'
|
||||||
import { RoofSegmentNode } from './roof-segment'
|
import { RoofSegmentNode } from './roof-segment'
|
||||||
|
|
||||||
export type RoofSurfaceMaterialRole = 'top' | 'edge' | 'wall'
|
export type RoofSurfaceMaterialRole = 'top' | 'edge' | 'wall'
|
||||||
export type RoofSurfaceMaterialSpec = {
|
export type RoofSurfaceMaterialSpec = {
|
||||||
material?: MaterialSchema
|
material?: MaterialSchemaType
|
||||||
materialPreset?: string
|
materialPreset?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RoofNode = BaseNode.extend({
|
export const RoofNode = BaseNode.extend({
|
||||||
id: objectId('roof'),
|
id: objectId('roof'),
|
||||||
type: nodeType('roof'),
|
type: nodeType('roof'),
|
||||||
material: MaterialSchemaSchema.optional(),
|
material: MaterialSchema.optional(),
|
||||||
materialPreset: z.string().optional(),
|
materialPreset: z.string().optional(),
|
||||||
topMaterial: MaterialSchemaSchema.optional(),
|
topMaterial: MaterialSchema.optional(),
|
||||||
topMaterialPreset: z.string().optional(),
|
topMaterialPreset: z.string().optional(),
|
||||||
edgeMaterial: MaterialSchemaSchema.optional(),
|
edgeMaterial: MaterialSchema.optional(),
|
||||||
edgeMaterialPreset: z.string().optional(),
|
edgeMaterialPreset: z.string().optional(),
|
||||||
wallMaterial: MaterialSchemaSchema.optional(),
|
wallMaterial: MaterialSchema.optional(),
|
||||||
wallMaterialPreset: z.string().optional(),
|
wallMaterialPreset: z.string().optional(),
|
||||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||||
// Rotation around Y axis in radians
|
// Rotation around Y axis in radians
|
||||||
@@ -54,7 +55,8 @@ export function getEffectiveRoofSurfaceMaterial(
|
|||||||
if (node.topMaterial !== undefined || typeof node.topMaterialPreset === 'string') {
|
if (node.topMaterial !== undefined || typeof node.topMaterialPreset === 'string') {
|
||||||
return {
|
return {
|
||||||
material: node.topMaterial,
|
material: node.topMaterial,
|
||||||
materialPreset: typeof node.topMaterialPreset === 'string' ? node.topMaterialPreset : undefined,
|
materialPreset:
|
||||||
|
typeof node.topMaterialPreset === 'string' ? node.topMaterialPreset : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import dedent from 'dedent'
|
import dedent from 'dedent'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { BaseNode, nodeType, objectId } from '../base'
|
import { BaseNode, nodeType, objectId } from '../base'
|
||||||
import { type MaterialSchema, MaterialSchema as MaterialSchemaSchema } from '../material'
|
import type { MaterialSchema as MaterialSchemaType } from '../material'
|
||||||
|
import { MaterialSchema } from '../material'
|
||||||
import { StairSegmentNode } from './stair-segment'
|
import { StairSegmentNode } from './stair-segment'
|
||||||
|
|
||||||
export const StairRailingMode = z.enum(['none', 'left', 'right', 'both'])
|
export const StairRailingMode = z.enum(['none', 'left', 'right', 'both'])
|
||||||
@@ -15,20 +16,20 @@ export type StairTopLandingMode = z.infer<typeof StairTopLandingMode>
|
|||||||
export type StairSlabOpeningMode = z.infer<typeof StairSlabOpeningMode>
|
export type StairSlabOpeningMode = z.infer<typeof StairSlabOpeningMode>
|
||||||
export type StairSurfaceMaterialRole = 'railing' | 'tread' | 'side'
|
export type StairSurfaceMaterialRole = 'railing' | 'tread' | 'side'
|
||||||
export type StairSurfaceMaterialSpec = {
|
export type StairSurfaceMaterialSpec = {
|
||||||
material?: MaterialSchema
|
material?: MaterialSchemaType
|
||||||
materialPreset?: string
|
materialPreset?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StairNode = BaseNode.extend({
|
export const StairNode = BaseNode.extend({
|
||||||
id: objectId('stair'),
|
id: objectId('stair'),
|
||||||
type: nodeType('stair'),
|
type: nodeType('stair'),
|
||||||
material: MaterialSchemaSchema.optional(),
|
material: MaterialSchema.optional(),
|
||||||
materialPreset: z.string().optional(),
|
materialPreset: z.string().optional(),
|
||||||
railingMaterial: MaterialSchemaSchema.optional(),
|
railingMaterial: MaterialSchema.optional(),
|
||||||
railingMaterialPreset: z.string().optional(),
|
railingMaterialPreset: z.string().optional(),
|
||||||
treadMaterial: MaterialSchemaSchema.optional(),
|
treadMaterial: MaterialSchema.optional(),
|
||||||
treadMaterialPreset: z.string().optional(),
|
treadMaterialPreset: z.string().optional(),
|
||||||
sideMaterial: MaterialSchemaSchema.optional(),
|
sideMaterial: MaterialSchema.optional(),
|
||||||
sideMaterialPreset: z.string().optional(),
|
sideMaterialPreset: z.string().optional(),
|
||||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||||
// Rotation around Y axis in radians
|
// Rotation around Y axis in radians
|
||||||
@@ -126,18 +127,26 @@ export function getEffectiveStairSurfaceMaterial(
|
|||||||
|
|
||||||
const treadFallback = {
|
const treadFallback = {
|
||||||
material: node.treadMaterial,
|
material: node.treadMaterial,
|
||||||
materialPreset: typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined,
|
materialPreset:
|
||||||
|
typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined,
|
||||||
}
|
}
|
||||||
const sideFallback = {
|
const sideFallback = {
|
||||||
material: node.sideMaterial,
|
material: node.sideMaterial,
|
||||||
materialPreset: typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined,
|
materialPreset:
|
||||||
|
typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role === 'tread' && (sideFallback.material !== undefined || sideFallback.materialPreset !== undefined)) {
|
if (
|
||||||
|
role === 'tread' &&
|
||||||
|
(sideFallback.material !== undefined || sideFallback.materialPreset !== undefined)
|
||||||
|
) {
|
||||||
return sideFallback
|
return sideFallback
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role === 'side' && (treadFallback.material !== undefined || treadFallback.materialPreset !== undefined)) {
|
if (
|
||||||
|
role === 'side' &&
|
||||||
|
(treadFallback.material !== undefined || treadFallback.materialPreset !== undefined)
|
||||||
|
) {
|
||||||
return treadFallback
|
return treadFallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ function buildMergedWallAttachmentUpdates(
|
|||||||
const wallChildren = [...(primary.children ?? []), ...(secondary.children ?? [])] as AnyNodeId[]
|
const wallChildren = [...(primary.children ?? []), ...(secondary.children ?? [])] as AnyNodeId[]
|
||||||
for (const childId of wallChildren) {
|
for (const childId of wallChildren) {
|
||||||
const child = nodes[childId]
|
const child = nodes[childId]
|
||||||
if (!child || !('position' in child) || !Array.isArray(child.position)) {
|
if (!(child && 'position' in child && Array.isArray(child.position))) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,10 +190,12 @@ function buildWallMergePlans(
|
|||||||
})
|
})
|
||||||
const [primary, secondary] = sortedCandidates
|
const [primary, secondary] = sortedCandidates
|
||||||
if (
|
if (
|
||||||
!primary ||
|
!(
|
||||||
!secondary ||
|
primary &&
|
||||||
!areWallStylesCompatible(primary, secondary) ||
|
secondary &&
|
||||||
!areWallsCollinearAcrossPoint(primary, secondary, junction)
|
areWallStylesCompatible(primary, secondary) &&
|
||||||
|
areWallsCollinearAcrossPoint(primary, secondary, junction)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -275,6 +277,7 @@ export const createNodesAction = (
|
|||||||
ops.forEach(({ node, parentId }) => {
|
ops.forEach(({ node, parentId }) => {
|
||||||
get().markDirty(node.id)
|
get().markDirty(node.id)
|
||||||
if (parentId) get().markDirty(parentId)
|
if (parentId) get().markDirty(parentId)
|
||||||
|
else if (node.parentId) get().markDirty(node.parentId as AnyNodeId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function migrateWallSurfaceMaterials(node: Record<string, any>) {
|
|||||||
materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
|
materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasInterior && !hasExterior) {
|
if (!(hasInterior || hasExterior)) {
|
||||||
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ function migrateStairSurfaceMaterials(node: Record<string, any>) {
|
|||||||
return legacyFinish
|
return legacyFinish
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasRailing && !hasTread && !hasSide) {
|
if (!(hasRailing || hasTread || hasSide)) {
|
||||||
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ function migrateRoofSurfaceMaterials(node: Record<string, any>) {
|
|||||||
materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
|
materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasTop && !hasEdge && !hasWall) {
|
if (!(hasTop || hasEdge || hasWall)) {
|
||||||
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNodeChildIds(node: AnyNode): AnyNodeId[] {
|
function getNodeChildIds(node: AnyNode): AnyNodeId[] {
|
||||||
if (!('children' in node) || !Array.isArray(node.children)) {
|
if (!('children' in node && Array.isArray(node.children))) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +511,13 @@ const useScene: UseSceneStore = create<SceneState>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
nodes: cleanedNodes,
|
||||||
|
rootNodeIds,
|
||||||
|
dirtyNodes: new Set<AnyNodeId>(),
|
||||||
|
collections: {},
|
||||||
|
})
|
||||||
|
|
||||||
const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds)
|
const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds)
|
||||||
const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds)
|
const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds)
|
||||||
if (normalizedRootNodeIds.length > 0) {
|
if (normalizedRootNodeIds.length > 0) {
|
||||||
@@ -701,8 +708,8 @@ let prevFutureLength = 0
|
|||||||
let prevNodesSnapshot: Record<AnyNodeId, AnyNode> | null = null
|
let prevNodesSnapshot: Record<AnyNodeId, AnyNode> | null = null
|
||||||
|
|
||||||
export function clearSceneHistory() {
|
export function clearSceneHistory() {
|
||||||
useScene.temporal.getState().clear()
|
|
||||||
resetSceneHistoryPauseDepth()
|
resetSceneHistoryPauseDepth()
|
||||||
|
useScene.temporal.getState().clear()
|
||||||
prevPastLength = 0
|
prevPastLength = 0
|
||||||
prevFutureLength = 0
|
prevFutureLength = 0
|
||||||
prevNodesSnapshot = null
|
prevNodesSnapshot = null
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-expect-error — bun:test is provided by the Bun runtime; core does not
|
|
||||||
// depend on @types/bun so the import type is unresolved at compile time.
|
|
||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
import type { AnyNode } from '../../schema'
|
import type { AnyNode } from '../../schema'
|
||||||
import { BuildingNode, LevelNode, SlabNode, StairNode, StairSegmentNode } from '../../schema'
|
import { BuildingNode, LevelNode, SlabNode, StairNode, StairSegmentNode } from '../../schema'
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
|
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync'
|
||||||
import type {
|
import type {
|
||||||
AnyNode,
|
AnyNode,
|
||||||
AnyNodeId,
|
AnyNodeId,
|
||||||
CeilingNode,
|
CeilingNode,
|
||||||
LevelNode,
|
|
||||||
SlabNode,
|
SlabNode,
|
||||||
StairNode,
|
StairNode,
|
||||||
StairSegmentNode,
|
StairSegmentNode,
|
||||||
} from '../../schema'
|
} from '../../schema'
|
||||||
|
|
||||||
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync'
|
|
||||||
import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint'
|
import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint'
|
||||||
|
|
||||||
type Point2D = [number, number]
|
type Point2D = [number, number]
|
||||||
@@ -36,10 +34,9 @@ type AxisAlignedRect = {
|
|||||||
maxZ: number
|
maxZ: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const CURVED_STAIR_SLAB_OPENING_RATIO = 0.9
|
const CURVED_STAIR_SLAB_OPENING_RATIO = 0.8
|
||||||
const STRAIGHT_STAIR_TARGET_THRESHOLD_MIN = 0.35
|
const STRAIGHT_STAIR_TARGET_THRESHOLD_MIN = 0.35
|
||||||
const STAIR_SLAB_OPENING_TIGHTENING = 0
|
const STAIR_SLAB_OPENING_TIGHTENING = 0
|
||||||
const CURVED_STAIR_OPENING_STEP_PADDING = 3
|
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, value))
|
return Math.min(max, Math.max(min, value))
|
||||||
@@ -280,7 +277,7 @@ function polygonArea(points: Point2D[]) {
|
|||||||
for (let index = 0; index < points.length; index += 1) {
|
for (let index = 0; index < points.length; index += 1) {
|
||||||
const current = points[index]
|
const current = points[index]
|
||||||
const next = points[(index + 1) % points.length]
|
const next = points[(index + 1) % points.length]
|
||||||
if (!current || !next) continue
|
if (!(current && next)) continue
|
||||||
area += current[0] * next[1] - next[0] * current[1]
|
area += current[0] * next[1] - next[0] * current[1]
|
||||||
}
|
}
|
||||||
return area / 2
|
return area / 2
|
||||||
@@ -426,39 +423,24 @@ function buildUnionPolygonsFromRects(rects: AxisAlignedRect[]): Point2D[][] {
|
|||||||
return polygons
|
return polygons
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurvedOpeningStepCount(
|
function getCurvedOpeningPolygon(stair: StairNode): Point2D[] {
|
||||||
stair: StairNode,
|
const width = Math.max(stair.width ?? 1, 0.4)
|
||||||
innerRadius: number,
|
const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9)
|
||||||
outerRadius: number,
|
const outerRadius = innerRadius + width
|
||||||
totalSweep: number,
|
const totalSweep = stair.sweepAngle ?? Math.PI / 2
|
||||||
) {
|
const openingSweep =
|
||||||
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10))
|
Math.sign(totalSweep || 1) *
|
||||||
const stepSweep = Math.abs(totalSweep) / stepCount
|
|
||||||
const midRadius = Math.max((innerRadius + outerRadius) * 0.5, 0.01)
|
|
||||||
const treadDepth = Math.max(stepSweep * midRadius, 0.2)
|
|
||||||
return Math.min(
|
|
||||||
stepCount,
|
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
Math.abs(totalSweep) * CURVED_STAIR_SLAB_OPENING_RATIO,
|
||||||
Math.ceil(1.8 / treadDepth),
|
Math.abs(totalSweep) / Math.max(stair.stepCount ?? 1, 1),
|
||||||
Math.ceil(stepCount * CURVED_STAIR_SLAB_OPENING_RATIO),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
const startAngle = totalSweep / 2 - openingSweep
|
||||||
|
const endAngle = totalSweep / 2
|
||||||
function buildArcOpeningPolygon(
|
|
||||||
stair: StairNode,
|
|
||||||
innerRadius: number,
|
|
||||||
outerRadius: number,
|
|
||||||
startAngle: number,
|
|
||||||
endAngle: number,
|
|
||||||
): Point2D[] {
|
|
||||||
const sweep = endAngle - startAngle
|
|
||||||
const segmentCount = Math.max(
|
const segmentCount = Math.max(
|
||||||
10,
|
10,
|
||||||
Math.min(
|
Math.min(
|
||||||
32,
|
32,
|
||||||
Math.ceil(Math.abs(sweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5),
|
Math.ceil(Math.abs(openingSweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const outerPoints: Point2D[] = []
|
const outerPoints: Point2D[] = []
|
||||||
@@ -466,7 +448,7 @@ function buildArcOpeningPolygon(
|
|||||||
|
|
||||||
for (let index = 0; index <= segmentCount; index++) {
|
for (let index = 0; index <= segmentCount; index++) {
|
||||||
const t = index / segmentCount
|
const t = index / segmentCount
|
||||||
const angle = startAngle + sweep * t
|
const angle = startAngle + (endAngle - startAngle) * t
|
||||||
outerPoints.push(
|
outerPoints.push(
|
||||||
toWorldPlanPoint(stair, Math.cos(angle) * outerRadius, Math.sin(angle) * outerRadius),
|
toWorldPlanPoint(stair, Math.cos(angle) * outerRadius, Math.sin(angle) * outerRadius),
|
||||||
)
|
)
|
||||||
@@ -474,8 +456,7 @@ function buildArcOpeningPolygon(
|
|||||||
|
|
||||||
for (let index = segmentCount; index >= 0; index--) {
|
for (let index = segmentCount; index >= 0; index--) {
|
||||||
const t = index / segmentCount
|
const t = index / segmentCount
|
||||||
const angle = startAngle + sweep * t
|
const angle = startAngle + (endAngle - startAngle) * t
|
||||||
|
|
||||||
innerPoints.push(
|
innerPoints.push(
|
||||||
toWorldPlanPoint(stair, Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius),
|
toWorldPlanPoint(stair, Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius),
|
||||||
)
|
)
|
||||||
@@ -484,39 +465,6 @@ function buildArcOpeningPolygon(
|
|||||||
return [...outerPoints, ...innerPoints]
|
return [...outerPoints, ...innerPoints]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurvedOpeningPolygon(stair: StairNode, targetElevation?: number): Point2D[] {
|
|
||||||
const width = Math.max(stair.width ?? 1, 0.4)
|
|
||||||
const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9)
|
|
||||||
const outerRadius = innerRadius + width
|
|
||||||
const totalSweep = stair.sweepAngle ?? Math.PI / 2
|
|
||||||
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10))
|
|
||||||
const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount
|
|
||||||
const stepSweep = totalSweep / stepCount
|
|
||||||
const targetThreshold = Math.max(stepHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN)
|
|
||||||
const endAngle = totalSweep / 2
|
|
||||||
|
|
||||||
const fallbackStartStepIndex = Math.max(
|
|
||||||
0,
|
|
||||||
stepCount - getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep),
|
|
||||||
)
|
|
||||||
let startStepIndex = fallbackStartStepIndex
|
|
||||||
if (typeof targetElevation === 'number') {
|
|
||||||
for (let index = 0; index < stepCount; index += 1) {
|
|
||||||
const stepTopElevation = stepHeight * (index + 1)
|
|
||||||
if (stepTopElevation >= targetElevation - targetThreshold) {
|
|
||||||
startStepIndex = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(fallbackStartStepIndex, index - CURVED_STAIR_OPENING_STEP_PADDING),
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startAngle = -totalSweep / 2 + stepSweep * startStepIndex
|
|
||||||
return buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSpiralOpeningPolygon(stair: StairNode): Point2D[] {
|
function getSpiralOpeningPolygon(stair: StairNode): Point2D[] {
|
||||||
const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4)
|
const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4)
|
||||||
const segmentCount = 48
|
const segmentCount = 48
|
||||||
@@ -621,7 +569,7 @@ function getStairOpeningPolygons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stair.stairType === 'curved') {
|
if (stair.stairType === 'curved') {
|
||||||
return [getCurvedOpeningPolygon(stair, targetElevation)]
|
return [getCurvedOpeningPolygon(stair)]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stair.stairType === 'spiral') {
|
if (stair.stairType === 'spiral') {
|
||||||
@@ -775,8 +723,7 @@ export function syncAutoStairOpenings(nodes: Record<string, AnyNode>) {
|
|||||||
const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)]
|
const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!polygonsEqual(existingHoles, nextHoles) ||
|
!(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata))
|
||||||
!metadataEqual(existingMetadata, nextMetadata)
|
|
||||||
) {
|
) {
|
||||||
updates.push({
|
updates.push({
|
||||||
id: slab.id,
|
id: slab.id,
|
||||||
@@ -826,8 +773,7 @@ export function syncAutoStairOpenings(nodes: Record<string, AnyNode>) {
|
|||||||
const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)]
|
const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!polygonsEqual(existingHoles, nextHoles) ||
|
!(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata))
|
||||||
!metadataEqual(existingMetadata, nextMetadata)
|
|
||||||
) {
|
) {
|
||||||
updates.push({
|
updates.push({
|
||||||
id: ceiling.id,
|
id: ceiling.id,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Point2D } from './wall-mitering'
|
|
||||||
import type { FenceNode, WallNode } from '../../schema'
|
import type { FenceNode, WallNode } from '../../schema'
|
||||||
|
import type { Point2D } from './wall-mitering'
|
||||||
|
|
||||||
const CURVE_EPSILON = 1e-6
|
const CURVE_EPSILON = 1e-6
|
||||||
const DEFAULT_SAMPLE_SEGMENTS = 24
|
const DEFAULT_SAMPLE_SEGMENTS = 24
|
||||||
@@ -115,7 +115,7 @@ function getWallArcData(wall: WallCurveLike) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const absSagitta = Math.abs(sagitta)
|
const absSagitta = Math.abs(sagitta)
|
||||||
const radius = chord.length * chord.length / (8 * absSagitta) + absSagitta / 2
|
const radius = (chord.length * chord.length) / (8 * absSagitta) + absSagitta / 2
|
||||||
const centerOffset = radius - absSagitta
|
const centerOffset = radius - absSagitta
|
||||||
const direction = Math.sign(sagitta) || 1
|
const direction = Math.sign(sagitta) || 1
|
||||||
const center = {
|
const center = {
|
||||||
@@ -183,7 +183,10 @@ export function getWallMidpointHandlePoint(wall: WallCurveLike) {
|
|||||||
|
|
||||||
export function sampleWallCenterline(wall: WallCurveLike, segments = DEFAULT_SAMPLE_SEGMENTS) {
|
export function sampleWallCenterline(wall: WallCurveLike, segments = DEFAULT_SAMPLE_SEGMENTS) {
|
||||||
const count = Math.max(1, segments)
|
const count = Math.max(1, segments)
|
||||||
return Array.from({ length: count + 1 }, (_, index) => getWallCurveFrameAt(wall, index / count).point)
|
return Array.from(
|
||||||
|
{ length: count + 1 },
|
||||||
|
(_, index) => getWallCurveFrameAt(wall, index / count).point,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWallCurveLength(wall: WallCurveLike, segments = DEFAULT_SAMPLE_SEGMENTS) {
|
export function getWallCurveLength(wall: WallCurveLike, segments = DEFAULT_SAMPLE_SEGMENTS) {
|
||||||
|
|||||||
@@ -135,10 +135,7 @@ function findJunctions(walls: WallNode[]): Map<string, Junction> {
|
|||||||
return actualJunctions
|
return actualJunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWallDirectionFromJunction(
|
function getWallDirectionFromJunction(wall: WallNode, endType: 'start' | 'end' | 'passthrough') {
|
||||||
wall: WallNode,
|
|
||||||
endType: 'start' | 'end' | 'passthrough',
|
|
||||||
) {
|
|
||||||
if (endType === 'passthrough') {
|
if (endType === 'passthrough') {
|
||||||
return {
|
return {
|
||||||
x: wall.end[0] - wall.start[0],
|
x: wall.end[0] - wall.start[0],
|
||||||
@@ -148,9 +145,7 @@ function getWallDirectionFromJunction(
|
|||||||
|
|
||||||
if (isCurvedWall(wall)) {
|
if (isCurvedWall(wall)) {
|
||||||
const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1)
|
const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1)
|
||||||
return endType === 'start'
|
return endType === 'start' ? frame.tangent : { x: -frame.tangent.x, y: -frame.tangent.y }
|
||||||
? frame.tangent
|
|
||||||
: { x: -frame.tangent.x, y: -frame.tangent.y }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return endType === 'start'
|
return endType === 'start'
|
||||||
@@ -158,18 +153,12 @@ function getWallDirectionFromJunction(
|
|||||||
: { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] }
|
: { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWallBoundaryFrame(
|
function getWallBoundaryFrame(wall: WallNode, endType: 'start' | 'end') {
|
||||||
wall: WallNode,
|
|
||||||
endType: 'start' | 'end',
|
|
||||||
) {
|
|
||||||
if (isCurvedWall(wall)) {
|
if (isCurvedWall(wall)) {
|
||||||
const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1)
|
const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1)
|
||||||
return {
|
return {
|
||||||
point: frame.point,
|
point: frame.point,
|
||||||
tangent:
|
tangent: endType === 'start' ? frame.tangent : { x: -frame.tangent.x, y: -frame.tangent.y },
|
||||||
endType === 'start'
|
|
||||||
? frame.tangent
|
|
||||||
: { x: -frame.tangent.x, y: -frame.tangent.y },
|
|
||||||
normal: frame.normal,
|
normal: frame.normal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"types": ["bun"]
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
@@ -44,24 +44,24 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^1.7.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"motion": "^12.34.3",
|
"motion": "^12.34.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"three-mesh-bvh": "^0.9.8",
|
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11",
|
||||||
|
"three-mesh-bvh": "~0.9.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal-app/core": "^0.7.0",
|
"@pascal-app/core": "^0.7.0",
|
||||||
"@pascal-app/viewer": "^0.7.0",
|
"@pascal-app/viewer": "^0.7.0",
|
||||||
"@pascal/typescript-config": "*",
|
"@pascal/typescript-config": "*",
|
||||||
|
"@types/bun": "^1.3.0",
|
||||||
"@types/howler": "^2.2.12",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/node": "^22.19.12",
|
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@types/three": "^0.184.0",
|
"@types/three": "^0.184.0",
|
||||||
"typescript": "5.9.3"
|
"typescript": "6.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,12 @@ export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{polygonDraftPolygonPoints && (
|
{polygonDraftPolygonPoints && (
|
||||||
<polygon fill={draftFill} fillOpacity={0.2} points={polygonDraftPolygonPoints} stroke="none" />
|
<polygon
|
||||||
|
fill={draftFill}
|
||||||
|
fillOpacity={0.2}
|
||||||
|
points={polygonDraftPolygonPoints}
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{polygonDraftPolylinePoints && (
|
{polygonDraftPolylinePoints && (
|
||||||
|
|||||||
@@ -125,7 +125,12 @@ export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLay
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{measurements.map((measurement) => (
|
{measurements.map((measurement) => (
|
||||||
<g className={className} key={measurement.id} pointerEvents="none" style={{ userSelect: 'none' }}>
|
<g
|
||||||
|
className={className}
|
||||||
|
key={measurement.id}
|
||||||
|
pointerEvents="none"
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
>
|
||||||
<FloorplanMeasurementLine
|
<FloorplanMeasurementLine
|
||||||
dashed={measurement.dashedExtensions ?? true}
|
dashed={measurement.dashedExtensions ?? true}
|
||||||
isSelected={measurement.isSelected}
|
isSelected={measurement.isSelected}
|
||||||
|
|||||||
@@ -396,8 +396,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|||||||
<>
|
<>
|
||||||
<polyline
|
<polyline
|
||||||
fill="none"
|
fill="none"
|
||||||
points={formatSvgPolygonPoints(arrow.polyline)}
|
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
|
points={formatSvgPolygonPoints(arrow.polyline)}
|
||||||
stroke={straightAccent}
|
stroke={straightAccent}
|
||||||
strokeWidth="1.15"
|
strokeWidth="1.15"
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect="non-scaling-stroke"
|
||||||
@@ -411,8 +411,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|||||||
/>
|
/>
|
||||||
<polygon
|
<polygon
|
||||||
fill={straightAccent}
|
fill={straightAccent}
|
||||||
points={formatSvgPolygonPoints(arrow.head)}
|
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
|
points={formatSvgPolygonPoints(arrow.head)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -438,8 +438,6 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
|
|
||||||
onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
|
|
||||||
onPointerDown={
|
onPointerDown={
|
||||||
canFocusStairs && stairSelected
|
canFocusStairs && stairSelected
|
||||||
? (event) => {
|
? (event) => {
|
||||||
@@ -449,6 +447,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
|
||||||
|
onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
|
||||||
pointerEvents={canSelectStairs ? undefined : 'none'}
|
pointerEvents={canSelectStairs ? undefined : 'none'}
|
||||||
style={canSelectStairs ? { cursor } : undefined}
|
style={canSelectStairs ? { cursor } : undefined}
|
||||||
>
|
>
|
||||||
@@ -456,8 +456,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|||||||
<polygon
|
<polygon
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
key={`${stair.id}:hit:${polygonIndex}`}
|
key={`${stair.id}:hit:${polygonIndex}`}
|
||||||
points={formatSvgPolygonPoints(polygon)}
|
|
||||||
pointerEvents={canSelectStairs ? 'all' : 'none'}
|
pointerEvents={canSelectStairs ? 'all' : 'none'}
|
||||||
|
points={formatSvgPolygonPoints(polygon)}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={hitStrokeWidth}
|
strokeWidth={hitStrokeWidth}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type CameraControlEvent,
|
type CameraControlEvent,
|
||||||
type CameraControlFitSceneEvent,
|
type CameraControlFitSceneEvent,
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer, ZONE_LAYER } from '@pascal-app/viewer'
|
import { useViewer, WalkthroughControls, ZONE_LAYER } from '@pascal-app/viewer'
|
||||||
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
||||||
import { useThree } from '@react-three/fiber'
|
import { useThree } from '@react-three/fiber'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|||||||
Executable → Regular
+4
-3
@@ -205,6 +205,7 @@ export function EditorLayoutV2({
|
|||||||
viewerContent,
|
viewerContent,
|
||||||
overlays,
|
overlays,
|
||||||
}: EditorLayoutV2Props) {
|
}: EditorLayoutV2Props) {
|
||||||
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@@ -229,7 +230,7 @@ export function EditorLayoutV2({
|
|||||||
|
|
||||||
{/* Main content: left column + right column */}
|
{/* Main content: left column + right column */}
|
||||||
<div className="flex min-h-0 flex-1">
|
<div className="flex min-h-0 flex-1">
|
||||||
{sidebarTabs.length > 0 && (
|
{!isCaptureMode && sidebarTabs.length > 0 && (
|
||||||
<LeftColumn
|
<LeftColumn
|
||||||
renderTabContent={renderTabContent}
|
renderTabContent={renderTabContent}
|
||||||
sidebarOverlay={sidebarOverlay}
|
sidebarOverlay={sidebarOverlay}
|
||||||
@@ -238,8 +239,8 @@ export function EditorLayoutV2({
|
|||||||
)}
|
)}
|
||||||
<RightColumn
|
<RightColumn
|
||||||
overlays={overlays}
|
overlays={overlays}
|
||||||
toolbarLeft={viewerToolbarLeft}
|
toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
|
||||||
toolbarRight={viewerToolbarRight}
|
toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
|
||||||
>
|
>
|
||||||
{viewerContent}
|
{viewerContent}
|
||||||
</RightColumn>
|
</RightColumn>
|
||||||
|
|||||||
@@ -455,14 +455,15 @@ export const FirstPersonControls = () => {
|
|||||||
{controllerStart && (
|
{controllerStart && (
|
||||||
<KeyboardControls map={keyboardMap}>
|
<KeyboardControls map={keyboardMap}>
|
||||||
<BVHEcctrl
|
<BVHEcctrl
|
||||||
ref={controllerRef}
|
acceleration={26}
|
||||||
key="first-person-controller"
|
airDragFactor={0.3}
|
||||||
colliderCapsuleArgs={[0.25, 0.8, 4, 8]}
|
colliderCapsuleArgs={[0.25, 0.8, 4, 8]}
|
||||||
colliderMeshes={[world.mesh]}
|
colliderMeshes={[world.mesh]}
|
||||||
collisionCheckIteration={3}
|
collisionCheckIteration={3}
|
||||||
collisionPushBackDamping={0.1}
|
collisionPushBackDamping={0.1}
|
||||||
collisionPushBackThreshold={0.001}
|
collisionPushBackThreshold={0.001}
|
||||||
debug={false}
|
debug={false}
|
||||||
|
deceleration={30}
|
||||||
delay={0}
|
delay={0}
|
||||||
fallGravityFactor={4}
|
fallGravityFactor={4}
|
||||||
floatCheckType="BOTH"
|
floatCheckType="BOTH"
|
||||||
@@ -473,13 +474,12 @@ export const FirstPersonControls = () => {
|
|||||||
floatSpringK={1200}
|
floatSpringK={1200}
|
||||||
gravity={9.81}
|
gravity={9.81}
|
||||||
jumpVel={6}
|
jumpVel={6}
|
||||||
|
key="first-person-controller"
|
||||||
maxRunSpeed={5.5}
|
maxRunSpeed={5.5}
|
||||||
maxSlope={1.2}
|
maxSlope={1.2}
|
||||||
maxWalkSpeed={4}
|
maxWalkSpeed={4}
|
||||||
position={controllerStart.position}
|
position={controllerStart.position}
|
||||||
acceleration={26}
|
ref={controllerRef}
|
||||||
airDragFactor={0.3}
|
|
||||||
deceleration={30}
|
|
||||||
/>
|
/>
|
||||||
</KeyboardControls>
|
</KeyboardControls>
|
||||||
)}
|
)}
|
||||||
@@ -551,12 +551,12 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|||||||
{isLocked && (
|
{isLocked && (
|
||||||
<div className="pointer-events-none fixed top-1/2 right-6 z-40 -translate-y-1/2">
|
<div className="pointer-events-none fixed top-1/2 right-6 z-40 -translate-y-1/2">
|
||||||
<div className="flex min-w-[148px] flex-col gap-3 rounded-2xl border border-border/35 bg-background/80 px-4 py-4 shadow-lg backdrop-blur-xl">
|
<div className="flex min-w-[148px] flex-col gap-3 rounded-2xl border border-border/35 bg-background/80 px-4 py-4 shadow-lg backdrop-blur-xl">
|
||||||
<ControlHint label="Move" keys={['W', 'A', 'S', 'D']} />
|
<ControlHint keys={['W', 'A', 'S', 'D']} label="Move" />
|
||||||
<div className="h-px w-full bg-border/30" />
|
<div className="h-px w-full bg-border/30" />
|
||||||
<InlineControlHint label="Jump" keyLabel="Space" />
|
<InlineControlHint keyLabel="Space" label="Jump" />
|
||||||
<InlineControlHint label="Sprint" keyLabel="Shift" />
|
<InlineControlHint keyLabel="Shift" label="Sprint" />
|
||||||
<InlineControlHint label="Interact" keyLabel="E / R" />
|
<InlineControlHint keyLabel="E / R" label="Interact" />
|
||||||
<InlineControlHint label="Close" keyLabel="T" />
|
<InlineControlHint keyLabel="T" label="Close" />
|
||||||
<div className="h-px w-full bg-border/30" />
|
<div className="h-px w-full bg-border/30" />
|
||||||
<span className="text-center text-muted-foreground/60 text-xs">
|
<span className="text-center text-muted-foreground/60 text-xs">
|
||||||
Click to look around
|
Click to look around
|
||||||
@@ -591,7 +591,7 @@ function ControlHint({ label, keys }: { label: string; keys: string[] }) {
|
|||||||
function InlineControlHint({ label, keyLabel }: { label: string; keyLabel: string }) {
|
function InlineControlHint({ label, keyLabel }: { label: string; keyLabel: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="font-medium text-[10px] text-muted-foreground/60 tracking-[0.03em] uppercase">
|
<span className="font-medium text-[10px] text-muted-foreground/60 uppercase tracking-[0.03em]">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<kbd className="flex h-5 min-w-5 items-center justify-center rounded border border-border/50 bg-accent/40 px-1.5 font-mono text-[10px] text-foreground/80 leading-none">
|
<kbd className="flex h-5 min-w-5 items-center justify-center rounded border border-border/50 bg-accent/40 px-1.5 font-mono text-[10px] text-foreground/80 leading-none">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
getGarageVisibleOpeningRatio,
|
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
type DoorNode,
|
type DoorNode,
|
||||||
|
getGarageVisibleOpeningRatio,
|
||||||
isOperationDoorType,
|
isOperationDoorType,
|
||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useInteractive,
|
useInteractive,
|
||||||
@@ -97,11 +97,11 @@ function shouldSkipColliderNode(nodeId: string, type: (typeof COLLIDER_NODE_TYPE
|
|||||||
const node = useScene.getState().nodes[nodeId as AnyNodeId]
|
const node = useScene.getState().nodes[nodeId as AnyNodeId]
|
||||||
if (!node || node.type !== 'door') return false
|
if (!node || node.type !== 'door') return false
|
||||||
|
|
||||||
if (node.openingKind === 'opening') return true
|
|
||||||
|
|
||||||
if (!node.segments.length) return true
|
if (!node.segments.length) return true
|
||||||
|
|
||||||
return node.segments.every((segment) => segment.type === 'empty')
|
if (node.openingKind === 'opening') return true
|
||||||
|
|
||||||
|
return node.segments.every((segment: { type: string }) => segment.type === 'empty')
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
||||||
@@ -273,9 +273,7 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mergedGeometry = mergeGeometries(geometries, false)
|
const mergedGeometry = mergeGeometries(geometries, false)
|
||||||
geometries.forEach((geometry) => {
|
geometries.forEach((geometry) => geometry.dispose())
|
||||||
geometry.dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) {
|
if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) {
|
||||||
mergedGeometry?.dispose()
|
mergedGeometry?.dispose()
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import '../../../three-types'
|
import '../../../three-types'
|
||||||
import { TransformControls, useKeyboardControls } from '@react-three/drei'
|
import { TransformControls, useKeyboardControls } from '@react-three/drei'
|
||||||
import { useFrame, useThree, type ThreeElements } from '@react-three/fiber'
|
import { type ThreeElements, useFrame, useThree } from '@react-three/fiber'
|
||||||
import {
|
|
||||||
Suspense,
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
} from 'react'
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { forwardRef, Suspense, useCallback, useImperativeHandle, useMemo, useRef } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { clamp } from 'three/src/math/MathUtils.js'
|
import { clamp } from 'three/src/math/MathUtils.js'
|
||||||
|
|
||||||
@@ -156,16 +149,7 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
const moveDirRef = useRef<THREE.ArrowHelper | null>(null)
|
const moveDirRef = useRef<THREE.ArrowHelper | null>(null)
|
||||||
const elapsedRef = useRef(0)
|
const elapsedRef = useRef(0)
|
||||||
|
|
||||||
function useIsInsideKeyboardControls() {
|
const [, getKeys] = useKeyboardControls()
|
||||||
try {
|
|
||||||
return !!useKeyboardControls()
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isInsideKeyboardControls = useIsInsideKeyboardControls()
|
|
||||||
const [_, getKeys] = isInsideKeyboardControls ? useKeyboardControls() : [null, null]
|
|
||||||
const presetKeys = {
|
const presetKeys = {
|
||||||
forward: false,
|
forward: false,
|
||||||
backward: false,
|
backward: false,
|
||||||
@@ -222,11 +206,11 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
const scaledContactRadiusVec = useRef(new THREE.Vector3())
|
const scaledContactRadiusVec = useRef(new THREE.Vector3())
|
||||||
const deltaDist = useRef(new THREE.Vector3())
|
const deltaDist = useRef(new THREE.Vector3())
|
||||||
const currSlopeAngle = useRef(0)
|
const currSlopeAngle = useRef(0)
|
||||||
const localMinDistance = useRef(Infinity)
|
const localMinDistance = useRef(Number.POSITIVE_INFINITY)
|
||||||
const localClosestPoint = useRef(new THREE.Vector3())
|
const localClosestPoint = useRef(new THREE.Vector3())
|
||||||
const localHitNormal = useRef(new THREE.Vector3())
|
const localHitNormal = useRef(new THREE.Vector3())
|
||||||
const triNormal = useRef(new THREE.Vector3())
|
const triNormal = useRef(new THREE.Vector3())
|
||||||
const globalMinDistance = useRef(Infinity)
|
const globalMinDistance = useRef(Number.POSITIVE_INFINITY)
|
||||||
const globalClosestPoint = useRef(new THREE.Vector3())
|
const globalClosestPoint = useRef(new THREE.Vector3())
|
||||||
const triHitPoint = useRef(new THREE.Vector3())
|
const triHitPoint = useRef(new THREE.Vector3())
|
||||||
const segHitPoint = useRef(new THREE.Vector3())
|
const segHitPoint = useRef(new THREE.Vector3())
|
||||||
@@ -286,7 +270,13 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
const moving = currentLinVel.current.lengthSq() > 1e-6
|
const moving = currentLinVel.current.lengthSq() > 1e-6
|
||||||
const platformIsMoving = totalPlatformDeltaPos.current.lengthSq() > 1e-6
|
const platformIsMoving = totalPlatformDeltaPos.current.lengthSq() > 1e-6
|
||||||
|
|
||||||
if (!moving && isOnGround.current && !jump && !isOnMovingPlatform.current && !platformIsMoving) {
|
if (
|
||||||
|
!moving &&
|
||||||
|
isOnGround.current &&
|
||||||
|
!jump &&
|
||||||
|
!isOnMovingPlatform.current &&
|
||||||
|
!platformIsMoving
|
||||||
|
) {
|
||||||
idleTime.current += delta
|
idleTime.current += delta
|
||||||
if (idleTime.current > sleepTimeout) isSleeping.current = true
|
if (idleTime.current > sleepTimeout) isSleeping.current = true
|
||||||
} else {
|
} else {
|
||||||
@@ -340,7 +330,10 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
upAxis.current,
|
upAxis.current,
|
||||||
)
|
)
|
||||||
characterModelTargetQuat.current.setFromRotationMatrix(characterModelLookMatrix.current)
|
characterModelTargetQuat.current.setFromRotationMatrix(characterModelLookMatrix.current)
|
||||||
characterModelRef.current.quaternion.slerp(characterModelTargetQuat.current, delta * turnSpeed)
|
characterModelRef.current.quaternion.slerp(
|
||||||
|
characterModelTargetQuat.current,
|
||||||
|
delta * turnSpeed,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSpeed = run ? maxRunSpeed : maxWalkSpeed
|
const maxSpeed = run ? maxRunSpeed : maxWalkSpeed
|
||||||
@@ -358,18 +351,33 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
)
|
)
|
||||||
currentLinVel.current.add(deltaLinVel.current)
|
currentLinVel.current.add(deltaLinVel.current)
|
||||||
} else if (isOnGround.current) {
|
} else if (isOnGround.current) {
|
||||||
deltaLinVel.current.copy(currentLinVelOnPlane.current).clampLength(0, deceleration * friction * delta)
|
deltaLinVel.current
|
||||||
|
.copy(currentLinVelOnPlane.current)
|
||||||
|
.clampLength(0, deceleration * friction * delta)
|
||||||
currentLinVel.current.sub(deltaLinVel.current)
|
currentLinVel.current.sub(deltaLinVel.current)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[acceleration, airDragFactor, counterAccFactor, deceleration, maxRunSpeed, maxWalkSpeed, turnSpeed, characterOrigin],
|
[
|
||||||
|
acceleration,
|
||||||
|
airDragFactor,
|
||||||
|
counterAccFactor,
|
||||||
|
deceleration,
|
||||||
|
maxRunSpeed,
|
||||||
|
maxWalkSpeed,
|
||||||
|
turnSpeed,
|
||||||
|
characterOrigin,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateSegmentBBox = useCallback(() => {
|
const updateSegmentBBox = useCallback(() => {
|
||||||
if (!characterGroupRef.current) return
|
if (!characterGroupRef.current) return
|
||||||
|
|
||||||
characterSegment.current.start.set(0, capsuleLength / 2, 0).add(characterGroupRef.current.position)
|
characterSegment.current.start
|
||||||
characterSegment.current.end.set(0, -capsuleLength / 2, 0).add(characterGroupRef.current.position)
|
.set(0, capsuleLength / 2, 0)
|
||||||
|
.add(characterGroupRef.current.position)
|
||||||
|
characterSegment.current.end
|
||||||
|
.set(0, -capsuleLength / 2, 0)
|
||||||
|
.add(characterGroupRef.current.position)
|
||||||
|
|
||||||
characterBbox.current
|
characterBbox.current
|
||||||
.makeEmpty()
|
.makeEmpty()
|
||||||
@@ -394,11 +402,18 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
|
|
||||||
const collisionCheck = useCallback(
|
const collisionCheck = useCallback(
|
||||||
(mesh: THREE.Mesh, originMatrix: THREE.Matrix4, delta: number) => {
|
(mesh: THREE.Mesh, originMatrix: THREE.Matrix4, delta: number) => {
|
||||||
if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeCollisionCheck) return
|
if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeCollisionCheck)
|
||||||
|
return
|
||||||
|
|
||||||
originMatrix.decompose(contactTempPos.current, contactTempQuat.current, contactTempScale.current)
|
originMatrix.decompose(
|
||||||
|
contactTempPos.current,
|
||||||
|
contactTempQuat.current,
|
||||||
|
contactTempScale.current,
|
||||||
|
)
|
||||||
collideInvertMatrix.current.copy(originMatrix).invert()
|
collideInvertMatrix.current.copy(originMatrix).invert()
|
||||||
localCharacterSegment.current.copy(characterSegment.current).applyMatrix4(collideInvertMatrix.current)
|
localCharacterSegment.current
|
||||||
|
.copy(characterSegment.current)
|
||||||
|
.applyMatrix4(collideInvertMatrix.current)
|
||||||
|
|
||||||
scaledContactRadiusVec.current.set(
|
scaledContactRadiusVec.current.set(
|
||||||
capsuleRadius / contactTempScale.current.x,
|
capsuleRadius / contactTempScale.current.x,
|
||||||
@@ -445,7 +460,10 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
contactDepth.current =
|
contactDepth.current =
|
||||||
capsuleRadius - capsuleContactPoint.current.distanceTo(triContactPoint.current)
|
capsuleRadius - capsuleContactPoint.current.distanceTo(triContactPoint.current)
|
||||||
|
|
||||||
accumulatedContactNormal.current.addScaledVector(contactNormal.current, contactDepth.current)
|
accumulatedContactNormal.current.addScaledVector(
|
||||||
|
contactNormal.current,
|
||||||
|
contactDepth.current,
|
||||||
|
)
|
||||||
accumulatedContactPoint.current.add(triContactPoint.current)
|
accumulatedContactPoint.current.add(triContactPoint.current)
|
||||||
totalDepth.current += contactDepth.current
|
totalDepth.current += contactDepth.current
|
||||||
triangleCount.current += 1
|
triangleCount.current += 1
|
||||||
@@ -492,14 +510,16 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
|
|
||||||
const floatingCheck = useCallback(
|
const floatingCheck = useCallback(
|
||||||
(mesh: THREE.Mesh, originMatrix: THREE.Matrix4) => {
|
(mesh: THREE.Mesh, originMatrix: THREE.Matrix4) => {
|
||||||
if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeFloatHit) return
|
if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeFloatHit) return
|
||||||
|
|
||||||
originMatrix.decompose(floatTempPos.current, floatTempQuat.current, floatTempScale.current)
|
originMatrix.decompose(floatTempPos.current, floatTempQuat.current, floatTempScale.current)
|
||||||
floatInvertMatrix.current.copy(originMatrix).invert()
|
floatInvertMatrix.current.copy(originMatrix).invert()
|
||||||
floatNormalInverseMatrix.current.getNormalMatrix(floatInvertMatrix.current)
|
floatNormalInverseMatrix.current.getNormalMatrix(floatInvertMatrix.current)
|
||||||
floatNormalMatrix.current.getNormalMatrix(originMatrix)
|
floatNormalMatrix.current.getNormalMatrix(originMatrix)
|
||||||
|
|
||||||
localFloatSensorSegment.current.copy(floatSensorSegment.current).applyMatrix4(floatInvertMatrix.current)
|
localFloatSensorSegment.current
|
||||||
|
.copy(floatSensorSegment.current)
|
||||||
|
.applyMatrix4(floatInvertMatrix.current)
|
||||||
localFloatSensorBboxExpendPoint.current
|
localFloatSensorBboxExpendPoint.current
|
||||||
.copy(floatSensorBboxExpendPoint.current)
|
.copy(floatSensorBboxExpendPoint.current)
|
||||||
.applyMatrix4(floatInvertMatrix.current)
|
.applyMatrix4(floatInvertMatrix.current)
|
||||||
@@ -517,20 +537,33 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
localFloatSensorBbox.current.min.addScaledVector(scaledFloatRadiusVec.current, -1)
|
localFloatSensorBbox.current.min.addScaledVector(scaledFloatRadiusVec.current, -1)
|
||||||
localFloatSensorBbox.current.max.add(scaledFloatRadiusVec.current)
|
localFloatSensorBbox.current.max.add(scaledFloatRadiusVec.current)
|
||||||
|
|
||||||
localMinDistance.current = Infinity
|
localMinDistance.current = Number.POSITIVE_INFINITY
|
||||||
localClosestPoint.current.set(Infinity, Infinity, Infinity)
|
localClosestPoint.current.set(
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
)
|
||||||
|
|
||||||
mesh.geometry.boundsTree.shapecast({
|
mesh.geometry.boundsTree.shapecast({
|
||||||
intersectsBounds: (box) => box.intersectsBox(localFloatSensorBbox.current),
|
intersectsBounds: (box) => box.intersectsBox(localFloatSensorBbox.current),
|
||||||
intersectsTriangle: (tri) => {
|
intersectsTriangle: (tri) => {
|
||||||
tri.closestPointToSegment(localFloatSensorSegment.current, triHitPoint.current, segHitPoint.current)
|
tri.closestPointToSegment(
|
||||||
localUpAxis.current.copy(upAxis.current).applyMatrix3(floatNormalInverseMatrix.current).normalize()
|
localFloatSensorSegment.current,
|
||||||
|
triHitPoint.current,
|
||||||
|
segHitPoint.current,
|
||||||
|
)
|
||||||
|
localUpAxis.current
|
||||||
|
.copy(upAxis.current)
|
||||||
|
.applyMatrix3(floatNormalInverseMatrix.current)
|
||||||
|
.normalize()
|
||||||
deltaHit.current.subVectors(triHitPoint.current, localFloatSensorSegment.current.start)
|
deltaHit.current.subVectors(triHitPoint.current, localFloatSensorSegment.current.start)
|
||||||
deltaHit.current.divide(scaledFloatRadiusVec.current)
|
deltaHit.current.divide(scaledFloatRadiusVec.current)
|
||||||
|
|
||||||
const totalLengthSq = deltaHit.current.lengthSq()
|
const totalLengthSq = deltaHit.current.lengthSq()
|
||||||
const dot = deltaHit.current.dot(localUpAxis.current)
|
const dot = deltaHit.current.dot(localUpAxis.current)
|
||||||
const verticalLength = Math.abs(dot) / ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius)
|
const verticalLength =
|
||||||
|
Math.abs(dot) /
|
||||||
|
((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius)
|
||||||
const horizontalLength = Math.sqrt(Math.max(0, totalLengthSq - dot * dot))
|
const horizontalLength = Math.sqrt(Math.max(0, totalLengthSq - dot * dot))
|
||||||
|
|
||||||
if (horizontalLength < 1 && verticalLength < 1) {
|
if (horizontalLength < 1 && verticalLength < 1) {
|
||||||
@@ -560,9 +593,14 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
const handleFloatingResponse = useCallback(
|
const handleFloatingResponse = useCallback(
|
||||||
(meshes: THREE.Mesh[], jump: boolean, delta: number) => {
|
(meshes: THREE.Mesh[], jump: boolean, delta: number) => {
|
||||||
if (meshes.length === 0) return
|
if (meshes.length === 0) return
|
||||||
|
let shouldJump = jump
|
||||||
|
|
||||||
globalMinDistance.current = Infinity
|
globalMinDistance.current = Number.POSITIVE_INFINITY
|
||||||
globalClosestPoint.current.set(Infinity, Infinity, Infinity)
|
globalClosestPoint.current.set(
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
Number.POSITIVE_INFINITY,
|
||||||
|
)
|
||||||
floatHitNormal.current.set(0, 1, 0)
|
floatHitNormal.current.set(0, 1, 0)
|
||||||
isOnGround.current = false
|
isOnGround.current = false
|
||||||
totalPlatformDeltaPos.current.set(0, 0, 0)
|
totalPlatformDeltaPos.current.set(0, 0, 0)
|
||||||
@@ -574,7 +612,11 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (floatCheckType !== 'SHAPECAST' && floatRaycastCandidates.length > 0 && globalMinDistance.current === Infinity) {
|
if (
|
||||||
|
floatCheckType !== 'SHAPECAST' &&
|
||||||
|
floatRaycastCandidates.length > 0 &&
|
||||||
|
globalMinDistance.current === Number.POSITIVE_INFINITY
|
||||||
|
) {
|
||||||
floatRaycaster.current.ray.origin.copy(floatSensorSegment.current.start)
|
floatRaycaster.current.ray.origin.copy(floatSensorSegment.current.start)
|
||||||
floatRaycaster.current.ray.direction.copy(gravityDir.current)
|
floatRaycaster.current.ray.direction.copy(gravityDir.current)
|
||||||
const hits = floatRaycaster.current.intersectObjects(floatRaycastCandidates, false)
|
const hits = floatRaycaster.current.intersectObjects(floatRaycastCandidates, false)
|
||||||
@@ -582,23 +624,28 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
if (hit?.point) {
|
if (hit?.point) {
|
||||||
globalClosestPoint.current.copy(hit.point)
|
globalClosestPoint.current.copy(hit.point)
|
||||||
if (hit.face) {
|
if (hit.face) {
|
||||||
floatHitNormal.current.copy(hit.face.normal).transformDirection(hit.object.matrixWorld).normalize()
|
floatHitNormal.current
|
||||||
|
.copy(hit.face.normal)
|
||||||
|
.transformDirection(hit.object.matrixWorld)
|
||||||
|
.normalize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalClosestPoint.current.x === Infinity) return
|
if (globalClosestPoint.current.x === Number.POSITIVE_INFINITY) return
|
||||||
|
|
||||||
relativeHitPoint.current.copy(globalClosestPoint.current).sub(floatSensorSegment.current.start)
|
relativeHitPoint.current
|
||||||
|
.copy(globalClosestPoint.current)
|
||||||
|
.sub(floatSensorSegment.current.start)
|
||||||
const currentDistance = relativeHitPoint.current.length()
|
const currentDistance = relativeHitPoint.current.length()
|
||||||
currSlopeAngle.current = floatHitNormal.current.angleTo(upAxis.current)
|
currSlopeAngle.current = floatHitNormal.current.angleTo(upAxis.current)
|
||||||
|
|
||||||
if (currentDistance < floatHeight + capsuleRadius) {
|
if (currentDistance < floatHeight + capsuleRadius) {
|
||||||
isOnGround.current = true
|
isOnGround.current = true
|
||||||
jump = false
|
shouldJump = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!jump) {
|
if (!shouldJump) {
|
||||||
const displacement = floatHeight + capsuleRadius - currentDistance
|
const displacement = floatHeight + capsuleRadius - currentDistance
|
||||||
const velocityOnHitNormal = currentLinVel.current.dot(floatHitNormal.current)
|
const velocityOnHitNormal = currentLinVel.current.dot(floatHitNormal.current)
|
||||||
const springForce = displacement * floatSpringK
|
const springForce = displacement * floatSpringK
|
||||||
@@ -608,7 +655,17 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
currentLinVel.current.addScaledVector(floatHitNormal.current, (totalForce / mass) * delta)
|
currentLinVel.current.addScaledVector(floatHitNormal.current, (totalForce / mass) * delta)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[capsuleRadius, floatCheckType, floatDampingC, floatHeight, floatRaycastCandidates, floatSpringK, floatingCheck, gravity, mass],
|
[
|
||||||
|
capsuleRadius,
|
||||||
|
floatCheckType,
|
||||||
|
floatDampingC,
|
||||||
|
floatHeight,
|
||||||
|
floatRaycastCandidates,
|
||||||
|
floatSpringK,
|
||||||
|
floatingCheck,
|
||||||
|
gravity,
|
||||||
|
mass,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateCharacterWithPlatform = useCallback(() => {
|
const updateCharacterWithPlatform = useCallback(() => {
|
||||||
@@ -646,8 +703,14 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const resetLinVel = useCallback(() => currentLinVel.current.set(0, 0, 0), [])
|
const resetLinVel = useCallback(() => currentLinVel.current.set(0, 0, 0), [])
|
||||||
const addLinVel = useCallback((velocity: THREE.Vector3) => currentLinVel.current.add(velocity), [])
|
const addLinVel = useCallback(
|
||||||
const setLinVel = useCallback((velocity: THREE.Vector3) => currentLinVel.current.copy(velocity), [])
|
(velocity: THREE.Vector3) => currentLinVel.current.add(velocity),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const setLinVel = useCallback(
|
||||||
|
(velocity: THREE.Vector3) => currentLinVel.current.copy(velocity),
|
||||||
|
[],
|
||||||
|
)
|
||||||
const setMovement = useCallback((movement: MovementInput) => {
|
const setMovement = useCallback((movement: MovementInput) => {
|
||||||
if (movement.forward !== undefined) forwardState.current = movement.forward
|
if (movement.forward !== undefined) forwardState.current = movement.forward
|
||||||
if (movement.backward !== undefined) backwardState.current = movement.backward
|
if (movement.backward !== undefined) backwardState.current = movement.backward
|
||||||
@@ -682,7 +745,9 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
debugRaySensorEnd.current?.position.copy(floatSensorSegment.current.end)
|
debugRaySensorEnd.current?.position.copy(floatSensorSegment.current.end)
|
||||||
standPointRef.current?.position.copy(globalClosestPoint.current)
|
standPointRef.current?.position.copy(globalClosestPoint.current)
|
||||||
if (characterGroupRef.current) {
|
if (characterGroupRef.current) {
|
||||||
lookDirRef.current?.position.copy(characterGroupRef.current.position).addScaledVector(upAxis.current, 0.7)
|
lookDirRef.current?.position
|
||||||
|
.copy(characterGroupRef.current.position)
|
||||||
|
.addScaledVector(upAxis.current, 0.7)
|
||||||
}
|
}
|
||||||
lookDirRef.current?.lookAt(lookDirRef.current.position.clone().add(camProjDir.current))
|
lookDirRef.current?.lookAt(lookDirRef.current.position.clone().add(camProjDir.current))
|
||||||
inputDirRef.current?.position.copy(characterSegment.current.end)
|
inputDirRef.current?.position.copy(characterSegment.current.end)
|
||||||
@@ -698,7 +763,7 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
if (paused || elapsedRef.current < delay) return
|
if (paused || elapsedRef.current < delay) return
|
||||||
|
|
||||||
const deltaTime = Math.min(1 / 45, delta) * slowMotionFactor
|
const deltaTime = Math.min(1 / 45, delta) * slowMotionFactor
|
||||||
const keys = isInsideKeyboardControls && getKeys ? getKeys() : presetKeys
|
const keys = getKeys() ?? presetKeys
|
||||||
const forward = forwardState.current || keys.forward
|
const forward = forwardState.current || keys.forward
|
||||||
const backward = backwardState.current || keys.backward
|
const backward = backwardState.current || keys.backward
|
||||||
const leftward = leftwardState.current || keys.leftward
|
const leftward = leftwardState.current || keys.leftward
|
||||||
@@ -740,7 +805,7 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<group {...props} ref={characterGroupRef} dispose={null}>
|
<group {...props} dispose={null} ref={characterGroupRef}>
|
||||||
{debug && (
|
{debug && (
|
||||||
<mesh ref={characterColliderRef}>
|
<mesh ref={characterColliderRef}>
|
||||||
<capsuleGeometry args={colliderCapsuleArgs} />
|
<capsuleGeometry args={colliderCapsuleArgs} />
|
||||||
@@ -777,8 +842,8 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|||||||
<octahedronGeometry args={[0.1, 0]} />
|
<octahedronGeometry args={[0.1, 0]} />
|
||||||
<meshNormalMaterial />
|
<meshNormalMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
<arrowHelper ref={inputDirRef} args={[undefined, undefined, undefined, '#00f']} />
|
<arrowHelper args={[undefined, undefined, undefined, '#00f']} ref={inputDirRef} />
|
||||||
<arrowHelper ref={moveDirRef} args={[undefined, undefined, undefined, '#f00']} />
|
<arrowHelper args={[undefined, undefined, undefined, '#f00']} ref={moveDirRef} />
|
||||||
<mesh ref={standPointRef}>
|
<mesh ref={standPointRef}>
|
||||||
<octahedronGeometry args={[0.12, 0]} />
|
<octahedronGeometry args={[0.12, 0]} />
|
||||||
<meshBasicMaterial color="red" opacity={0.2} transparent />
|
<meshBasicMaterial color="red" opacity={0.2} transparent />
|
||||||
|
|||||||
Executable → Regular
@@ -4092,7 +4092,6 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
d={path}
|
d={path}
|
||||||
fill={isDeleteHovered ? palette.deleteFill : palette.slabFill}
|
fill={isDeleteHovered ? palette.deleteFill : palette.slabFill}
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
opacity={slabFillOpacity}
|
|
||||||
onClick={
|
onClick={
|
||||||
canSelectSlabs
|
canSelectSlabs
|
||||||
? (event) => {
|
? (event) => {
|
||||||
@@ -4111,9 +4110,10 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
}
|
}
|
||||||
onPointerEnter={canSelectSlabs ? () => onSlabHoverChange(slab.id) : undefined}
|
onPointerEnter={canSelectSlabs ? () => onSlabHoverChange(slab.id) : undefined}
|
||||||
onPointerLeave={canSelectSlabs ? () => onSlabHoverChange(null) : undefined}
|
onPointerLeave={canSelectSlabs ? () => onSlabHoverChange(null) : undefined}
|
||||||
|
opacity={slabFillOpacity}
|
||||||
pointerEvents={canSelectSlabs ? undefined : 'none'}
|
pointerEvents={canSelectSlabs ? undefined : 'none'}
|
||||||
style={canSelectSlabs ? { cursor: EDITOR_CURSOR } : undefined}
|
|
||||||
stroke="none"
|
stroke="none"
|
||||||
|
style={canSelectSlabs ? { cursor: EDITOR_CURSOR } : undefined}
|
||||||
/>
|
/>
|
||||||
{isSelected && !isDeleteHovered ? (
|
{isSelected && !isDeleteHovered ? (
|
||||||
<path
|
<path
|
||||||
@@ -4168,7 +4168,6 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
d={path}
|
d={path}
|
||||||
fill={isDeleteHovered ? palette.deleteFill : palette.ceilingFill}
|
fill={isDeleteHovered ? palette.deleteFill : palette.ceilingFill}
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
opacity={ceilingFillOpacity}
|
|
||||||
onClick={
|
onClick={
|
||||||
canSelectCeilings
|
canSelectCeilings
|
||||||
? (event) => {
|
? (event) => {
|
||||||
@@ -4189,9 +4188,10 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
canSelectCeilings ? () => onCeilingHoverChange(ceiling.id) : undefined
|
canSelectCeilings ? () => onCeilingHoverChange(ceiling.id) : undefined
|
||||||
}
|
}
|
||||||
onPointerLeave={canSelectCeilings ? () => onCeilingHoverChange(null) : undefined}
|
onPointerLeave={canSelectCeilings ? () => onCeilingHoverChange(null) : undefined}
|
||||||
|
opacity={ceilingFillOpacity}
|
||||||
pointerEvents={canSelectCeilings ? undefined : 'none'}
|
pointerEvents={canSelectCeilings ? undefined : 'none'}
|
||||||
style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined}
|
|
||||||
stroke="none"
|
stroke="none"
|
||||||
|
style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined}
|
||||||
/>
|
/>
|
||||||
{isSelected && !isDeleteHovered ? (
|
{isSelected && !isDeleteHovered ? (
|
||||||
<path
|
<path
|
||||||
@@ -4298,8 +4298,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
<polygon
|
<polygon
|
||||||
fill={`url(#${wallSelectionHatchId})`}
|
fill={`url(#${wallSelectionHatchId})`}
|
||||||
opacity={1}
|
opacity={1}
|
||||||
points={points}
|
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
|
points={points}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</g>
|
</g>
|
||||||
@@ -4427,8 +4427,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
{canSelectGeometry && (
|
{canSelectGeometry && (
|
||||||
<polygon
|
<polygon
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
points={points}
|
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
|
points={points}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect="non-scaling-stroke"
|
||||||
@@ -4538,8 +4538,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
{canSelectGeometry && (
|
{canSelectGeometry && (
|
||||||
<polygon
|
<polygon
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
points={points}
|
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
|
points={points}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect="non-scaling-stroke"
|
||||||
@@ -5218,8 +5218,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
{canSelectGeometry && (
|
{canSelectGeometry && (
|
||||||
<polygon
|
<polygon
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
points={points}
|
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
|
points={points}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect="non-scaling-stroke"
|
||||||
@@ -5290,8 +5290,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|||||||
d={foldingPath}
|
d={foldingPath}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={doorStroke}
|
stroke={doorStroke}
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
strokeWidth={isSelected || isSelectionHighlighted ? '1.8' : '1.25'}
|
strokeWidth={isSelected || isSelectionHighlighted ? '1.8' : '1.25'}
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect="non-scaling-stroke"
|
||||||
/>
|
/>
|
||||||
@@ -5963,6 +5963,12 @@ function FloorplanItemImage({
|
|||||||
}) {
|
}) {
|
||||||
const resolvedUrl = useResolvedAssetUrl(url)
|
const resolvedUrl = useResolvedAssetUrl(url)
|
||||||
if (!resolvedUrl) return null
|
if (!resolvedUrl) return null
|
||||||
|
// The PNG is captured with the modal's top-down camera (default up = +Y),
|
||||||
|
// so its pixel-right is world +X and pixel-up is world -Z. The plan SVG
|
||||||
|
// negates both axes (`toSvgX(v) = -v`, `toSvgY(v) = -v`), which together
|
||||||
|
// are a 180° rotation — so the captured image lands upside-down /
|
||||||
|
// mirrored when overlaid as-is. Bake that 180° into the image transform
|
||||||
|
// here; the panel / modal previews use the PNG directly and stay correct.
|
||||||
const rotationDeg = (-rotation * 180) / Math.PI + 180
|
const rotationDeg = (-rotation * 180) / Math.PI + 180
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
@@ -6168,6 +6174,7 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({
|
|||||||
}
|
}
|
||||||
points={points}
|
points={points}
|
||||||
stroke={stroke}
|
stroke={stroke}
|
||||||
|
strokeOpacity={1}
|
||||||
strokeWidth={
|
strokeWidth={
|
||||||
isSelectionActive ? FLOORPLAN_SELECTED_WALL_STROKE_WIDTH : FLOORPLAN_WALL_STROKE_WIDTH
|
isSelectionActive ? FLOORPLAN_SELECTED_WALL_STROKE_WIDTH : FLOORPLAN_WALL_STROKE_WIDTH
|
||||||
}
|
}
|
||||||
@@ -6217,8 +6224,8 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({
|
|||||||
<polygon
|
<polygon
|
||||||
fill={`url(#${wallSelectionHatchId})`}
|
fill={`url(#${wallSelectionHatchId})`}
|
||||||
opacity={1}
|
opacity={1}
|
||||||
points={points}
|
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
|
points={points}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{itemDimensionMeasurements.length > 0 ? (
|
{itemDimensionMeasurements.length > 0 ? (
|
||||||
@@ -6308,8 +6315,8 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({
|
|||||||
<polygon
|
<polygon
|
||||||
fill={fill}
|
fill={fill}
|
||||||
fillOpacity={isDeleteHovered ? 0.82 : 0.92}
|
fillOpacity={isDeleteHovered ? 0.82 : 0.92}
|
||||||
points={FLOORPLAN_SPAWN_ARROW_POINTS}
|
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
|
points={FLOORPLAN_SPAWN_ARROW_POINTS}
|
||||||
stroke={stroke}
|
stroke={stroke}
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={0.055}
|
strokeWidth={0.055}
|
||||||
@@ -7224,6 +7231,11 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|||||||
y2={endSvg.y}
|
y2={endSvg.y}
|
||||||
/>
|
/>
|
||||||
<line
|
<line
|
||||||
|
onPointerDown={
|
||||||
|
onEdgePointerDown
|
||||||
|
? (event) => onEdgePointerDown(nodeId, edgeIndex, event)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
pointerEvents="stroke"
|
pointerEvents="stroke"
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
@@ -7234,11 +7246,6 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|||||||
x2={endSvg.x}
|
x2={endSvg.x}
|
||||||
y1={startSvg.y}
|
y1={startSvg.y}
|
||||||
y2={endSvg.y}
|
y2={endSvg.y}
|
||||||
onPointerDown={
|
|
||||||
onEdgePointerDown
|
|
||||||
? (event) => onEdgePointerDown(nodeId, edgeIndex, event)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
@@ -9838,7 +9845,7 @@ export function FloorplanPanel() {
|
|||||||
zoneVertexDragState != null ||
|
zoneVertexDragState != null ||
|
||||||
isPolygonDraftBuildActive
|
isPolygonDraftBuildActive
|
||||||
|
|
||||||
if (!hasUserAdjustedViewportRef.current && !transientFloorplanFit) {
|
if (!(hasUserAdjustedViewportRef.current || transientFloorplanFit)) {
|
||||||
setViewport((current) =>
|
setViewport((current) =>
|
||||||
floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport,
|
floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport,
|
||||||
)
|
)
|
||||||
@@ -10970,6 +10977,14 @@ export function FloorplanPanel() {
|
|||||||
}
|
}
|
||||||
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasPendingItemMeshFootprints) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleMovingFloorplanNodeRefresh()
|
||||||
|
}, [hasPendingItemMeshFootprints, scheduleMovingFloorplanNodeRefresh])
|
||||||
|
|
||||||
// Subscribe to the live-transforms store so rotation/position changes that
|
// Subscribe to the live-transforms store so rotation/position changes that
|
||||||
// *don't* go through pointer events still refresh the floorplan — e.g. R/T
|
// *don't* go through pointer events still refresh the floorplan — e.g. R/T
|
||||||
// keyboard rotation during placement updates `useLiveTransforms` but emits
|
// keyboard rotation during placement updates `useLiveTransforms` but emits
|
||||||
@@ -10985,14 +11000,6 @@ export function FloorplanPanel() {
|
|||||||
return unsubscribe
|
return unsubscribe
|
||||||
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasPendingItemMeshFootprints) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleMovingFloorplanNodeRefresh()
|
|
||||||
}, [hasPendingItemMeshFootprints, scheduleMovingFloorplanNodeRefresh])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(movingNode?.type === 'door' || movingNode?.type === 'window')) {
|
if (!(movingNode?.type === 'door' || movingNode?.type === 'window')) {
|
||||||
return
|
return
|
||||||
@@ -16132,17 +16139,13 @@ export function FloorplanPanel() {
|
|||||||
onDuplicate: handleSelectedItemDuplicate,
|
onDuplicate: handleSelectedItemDuplicate,
|
||||||
onMove: handleSelectedItemMove,
|
onMove: handleSelectedItemMove,
|
||||||
}}
|
}}
|
||||||
|
offsetY={FLOORPLAN_ACTION_MENU_OFFSET_Y}
|
||||||
opening={{
|
opening={{
|
||||||
position: selectedOpeningActionMenuPosition,
|
position: selectedOpeningActionMenuPosition,
|
||||||
onDelete: handleSelectedOpeningDelete,
|
onDelete: handleSelectedOpeningDelete,
|
||||||
onDuplicate: handleSelectedOpeningDuplicate,
|
onDuplicate: handleSelectedOpeningDuplicate,
|
||||||
onMove: handleSelectedOpeningMove,
|
onMove: handleSelectedOpeningMove,
|
||||||
}}
|
}}
|
||||||
spawn={{
|
|
||||||
position: selectedSpawnActionMenuPosition,
|
|
||||||
onDelete: handleSelectedSpawnDelete,
|
|
||||||
onMove: handleSelectedSpawnMove,
|
|
||||||
}}
|
|
||||||
roof={{
|
roof={{
|
||||||
position: selectedRoofActionMenuPosition,
|
position: selectedRoofActionMenuPosition,
|
||||||
onDelete: handleSelectedRoofDelete,
|
onDelete: handleSelectedRoofDelete,
|
||||||
@@ -16157,6 +16160,11 @@ export function FloorplanPanel() {
|
|||||||
: handleSelectedSlabDelete,
|
: handleSelectedSlabDelete,
|
||||||
onMove: selectedSlabEditingHole ? handleSelectedSlabHoleMove : handleSelectedSlabMove,
|
onMove: selectedSlabEditingHole ? handleSelectedSlabHoleMove : handleSelectedSlabMove,
|
||||||
}}
|
}}
|
||||||
|
spawn={{
|
||||||
|
position: selectedSpawnActionMenuPosition,
|
||||||
|
onDelete: handleSelectedSpawnDelete,
|
||||||
|
onMove: handleSelectedSpawnMove,
|
||||||
|
}}
|
||||||
stair={{
|
stair={{
|
||||||
position: selectedStairActionMenuPosition,
|
position: selectedStairActionMenuPosition,
|
||||||
onDelete: handleSelectedStairDelete,
|
onDelete: handleSelectedStairDelete,
|
||||||
@@ -16168,7 +16176,6 @@ export function FloorplanPanel() {
|
|||||||
onDelete: handleSelectedWallDelete,
|
onDelete: handleSelectedWallDelete,
|
||||||
onMove: handleSelectedWallMove,
|
onMove: handleSelectedWallMove,
|
||||||
}}
|
}}
|
||||||
offsetY={FLOORPLAN_ACTION_MENU_OFFSET_Y}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{referenceScaleDraft && (
|
{referenceScaleDraft && (
|
||||||
@@ -16201,7 +16208,7 @@ export function FloorplanPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3 rounded-xl border border-border/70 bg-white/5 px-3 py-2">
|
<div className="mb-3 rounded-xl border border-border/70 bg-white/5 px-3 py-2">
|
||||||
<div className="text-muted-foreground text-[11px] uppercase tracking-wide">
|
<div className="text-[11px] text-muted-foreground uppercase tracking-wide">
|
||||||
Drawn line
|
Drawn line
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 font-medium text-sm">
|
<div className="mt-1 font-medium text-sm">
|
||||||
@@ -16363,8 +16370,8 @@ export function FloorplanPanel() {
|
|||||||
<FloorplanGuideLayer
|
<FloorplanGuideLayer
|
||||||
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
||||||
activeGuideInteractionMode={activeGuideInteractionMode}
|
activeGuideInteractionMode={activeGuideInteractionMode}
|
||||||
guideUi={guideUi}
|
|
||||||
guides={displayGuides}
|
guides={displayGuides}
|
||||||
|
guideUi={guideUi}
|
||||||
isInteractive={canInteractWithGuides}
|
isInteractive={canInteractWithGuides}
|
||||||
onGuideSelect={handleGuideSelect}
|
onGuideSelect={handleGuideSelect}
|
||||||
onGuideTranslateStart={handleGuideTranslateStart}
|
onGuideTranslateStart={handleGuideTranslateStart}
|
||||||
@@ -16385,6 +16392,8 @@ export function FloorplanPanel() {
|
|||||||
hoveredSlabId={hoveredSlabId}
|
hoveredSlabId={hoveredSlabId}
|
||||||
hoveredWallId={hoveredWallId}
|
hoveredWallId={hoveredWallId}
|
||||||
isDeleteMode={isDeleteMode}
|
isDeleteMode={isDeleteMode}
|
||||||
|
isGuideTraceVisible={isGuideTraceVisible}
|
||||||
|
metersPerUnit={calibratedMetersPerUnit}
|
||||||
onCeilingDoubleClick={handleCeilingDoubleClick}
|
onCeilingDoubleClick={handleCeilingDoubleClick}
|
||||||
onCeilingHoverChange={handleCeilingHoverChange}
|
onCeilingHoverChange={handleCeilingHoverChange}
|
||||||
onCeilingSelect={handleCeilingSelect}
|
onCeilingSelect={handleCeilingSelect}
|
||||||
@@ -16401,11 +16410,9 @@ export function FloorplanPanel() {
|
|||||||
openingsPolygons={openingsPolygons}
|
openingsPolygons={openingsPolygons}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
selectedIdSet={selectedIdSet}
|
selectedIdSet={selectedIdSet}
|
||||||
slabSelectionHatchId={slabSelectionHatchId}
|
|
||||||
slabPolygons={displaySlabPolygons}
|
slabPolygons={displaySlabPolygons}
|
||||||
|
slabSelectionHatchId={slabSelectionHatchId}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
metersPerUnit={calibratedMetersPerUnit}
|
|
||||||
isGuideTraceVisible={isGuideTraceVisible}
|
|
||||||
wallPolygons={displayWallPolygons}
|
wallPolygons={displayWallPolygons}
|
||||||
wallSelectionHatchId={wallSelectionHatchId}
|
wallSelectionHatchId={wallSelectionHatchId}
|
||||||
/>
|
/>
|
||||||
@@ -16483,8 +16490,8 @@ export function FloorplanPanel() {
|
|||||||
|
|
||||||
<FloorplanReferenceScaleLayer
|
<FloorplanReferenceScaleLayer
|
||||||
draft={referenceScaleDraft}
|
draft={referenceScaleDraft}
|
||||||
guideUi={guideUi}
|
|
||||||
guides={displayGuides}
|
guides={displayGuides}
|
||||||
|
guideUi={guideUi}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
unitsPerPixel={floorplanUnitsPerPixel}
|
unitsPerPixel={floorplanUnitsPerPixel}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { ViewerOverlay } from '../../components/viewer-overlay'
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
||||||
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
||||||
import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
|
import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
|
||||||
|
import { useAutoFrame } from '../../hooks/use-auto-frame'
|
||||||
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
||||||
import { useKeyboard } from '../../hooks/use-keyboard'
|
import { useKeyboard } from '../../hooks/use-keyboard'
|
||||||
import {
|
import {
|
||||||
@@ -64,6 +65,7 @@ import { Grid } from './grid'
|
|||||||
import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
|
import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
|
||||||
import { SelectionManager } from './selection-manager'
|
import { SelectionManager } from './selection-manager'
|
||||||
import { SiteEdgeLabels } from './site-edge-labels'
|
import { SiteEdgeLabels } from './site-edge-labels'
|
||||||
|
import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
|
||||||
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
|
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
|
||||||
import { WallMeasurementLabel } from './wall-measurement-label'
|
import { WallMeasurementLabel } from './wall-measurement-label'
|
||||||
|
|
||||||
@@ -76,12 +78,12 @@ const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|||||||
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
||||||
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
||||||
const EDITOR_HOVER_STYLES: HoverStyles = {
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
||||||
default: { visibleColor: 0x00_aaff, hiddenColor: 0xf3_ff47, strength: 5, pulse: true },
|
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
||||||
delete: { visibleColor: 0xef_4444, hiddenColor: 0x99_1b1b, strength: 6, pulse: false },
|
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
||||||
'paint-ready': { visibleColor: 0xf5_9e0b, hiddenColor: 0xfd_e068, strength: 5, pulse: true },
|
'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
|
||||||
'paint-disabled': {
|
'paint-disabled': {
|
||||||
visibleColor: 0x94_a3b8,
|
visibleColor: 0x94_a3_b8,
|
||||||
hiddenColor: 0x47_5569,
|
hiddenColor: 0x47_55_69,
|
||||||
strength: 4,
|
strength: 4,
|
||||||
pulse: false,
|
pulse: false,
|
||||||
},
|
},
|
||||||
@@ -462,7 +464,7 @@ function ViewerCanvasControlsHint({
|
|||||||
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
||||||
<section
|
<section
|
||||||
aria-label="Camera controls hint"
|
aria-label="Camera controls hint"
|
||||||
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-[0_22px_40px_-28px_rgba(15,23,42,0.65),0_10px_24px_-20px_rgba(15,23,42,0.55)] backdrop-blur-xl"
|
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
|
||||||
>
|
>
|
||||||
<div className="grid min-w-0 flex-1 grid-cols-3 items-start divide-x divide-border/18">
|
<div className="grid min-w-0 flex-1 grid-cols-3 items-start divide-x divide-border/18">
|
||||||
{hints.map((hint) => (
|
{hints.map((hint) => (
|
||||||
@@ -753,7 +755,7 @@ function PaintCursorLayer({
|
|||||||
(activePaintMaterial.material !== undefined ||
|
(activePaintMaterial.material !== undefined ||
|
||||||
activePaintMaterial.materialPreset !== undefined),
|
activePaintMaterial.materialPreset !== undefined),
|
||||||
)
|
)
|
||||||
const label = !hasMaterial ? 'Choose material' : `Paint ${activePaintTarget}`
|
const label = hasMaterial ? `Paint ${activePaintTarget}` : 'Choose material'
|
||||||
const icon = 'mdi:format-color-fill'
|
const icon = 'mdi:format-color-fill'
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -786,16 +788,16 @@ function PaintCursorLayer({
|
|||||||
const ViewerCanvas = memo(function ViewerCanvas({
|
const ViewerCanvas = memo(function ViewerCanvas({
|
||||||
isVersionPreviewMode,
|
isVersionPreviewMode,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isFirstPersonMode,
|
||||||
hasLoadedInitialScene,
|
hasLoadedInitialScene,
|
||||||
showLoader,
|
showLoader,
|
||||||
isFirstPersonMode,
|
|
||||||
onThumbnailCapture,
|
onThumbnailCapture,
|
||||||
}: {
|
}: {
|
||||||
isVersionPreviewMode: boolean
|
isVersionPreviewMode: boolean
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
isFirstPersonMode: boolean
|
||||||
hasLoadedInitialScene: boolean
|
hasLoadedInitialScene: boolean
|
||||||
showLoader: boolean
|
showLoader: boolean
|
||||||
isFirstPersonMode: boolean
|
|
||||||
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
||||||
}) {
|
}) {
|
||||||
const viewMode = useEditor((s) => s.viewMode)
|
const viewMode = useEditor((s) => s.viewMode)
|
||||||
@@ -837,7 +839,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|||||||
window.removeEventListener('pointermove', handlePointerMove)
|
window.removeEventListener('pointermove', handlePointerMove)
|
||||||
window.removeEventListener('pointerup', handlePointerUp)
|
window.removeEventListener('pointerup', handlePointerUp)
|
||||||
}
|
}
|
||||||
}, [setFloorplanPaneRatio])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
|
setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
|
||||||
@@ -940,7 +942,9 @@ export default function Editor({
|
|||||||
commandPaletteEmptyAction,
|
commandPaletteEmptyAction,
|
||||||
}: EditorProps) {
|
}: EditorProps) {
|
||||||
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
||||||
|
|
||||||
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
|
||||||
|
|
||||||
const { isLoadingSceneRef } = useAutoSave({
|
const { isLoadingSceneRef } = useAutoSave({
|
||||||
onSave,
|
onSave,
|
||||||
onDirty,
|
onDirty,
|
||||||
@@ -951,8 +955,7 @@ export default function Editor({
|
|||||||
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
||||||
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
||||||
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
||||||
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
||||||
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
||||||
|
|
||||||
const sidebarWidth = useSidebarStore((s) => s.width)
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
||||||
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
||||||
@@ -970,39 +973,6 @@ export default function Editor({
|
|||||||
}
|
}
|
||||||
}, [projectId])
|
}, [projectId])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const wasFirstPersonMode = wasFirstPersonModeRef.current
|
|
||||||
wasFirstPersonModeRef.current = isFirstPersonMode
|
|
||||||
|
|
||||||
if (isFirstPersonMode && !wasFirstPersonMode) {
|
|
||||||
const viewer = useViewer.getState()
|
|
||||||
firstPersonPreviousLevelRef.current = viewer.selection.levelId
|
|
||||||
viewer.setCameraMode('perspective')
|
|
||||||
viewer.setWallMode('up')
|
|
||||||
viewer.setWalkthroughMode(true)
|
|
||||||
viewer.setSelection({ selectedIds: [], zoneId: null })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(wasFirstPersonMode && !isFirstPersonMode)) return
|
|
||||||
|
|
||||||
const viewer = useViewer.getState()
|
|
||||||
const previousLevelId = firstPersonPreviousLevelRef.current
|
|
||||||
firstPersonPreviousLevelRef.current = null
|
|
||||||
viewer.setWalkthroughMode(false)
|
|
||||||
|
|
||||||
if (!previousLevelId) return
|
|
||||||
|
|
||||||
const previousLevelNode = useScene.getState().nodes[previousLevelId]
|
|
||||||
if (previousLevelNode?.type === 'level') {
|
|
||||||
viewer.setSelection({
|
|
||||||
levelId: previousLevelId,
|
|
||||||
zoneId: null,
|
|
||||||
selectedIds: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [isFirstPersonMode])
|
|
||||||
|
|
||||||
// Load scene on mount (or when onLoad identity changes, e.g. project switch)
|
// Load scene on mount (or when onLoad identity changes, e.g. project switch)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -1064,6 +1034,42 @@ export default function Editor({
|
|||||||
|
|
||||||
const showLoader = isLoading || isSceneLoading
|
const showLoader = isLoading || isSceneLoading
|
||||||
|
|
||||||
|
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
||||||
|
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wasFirstPersonMode = wasFirstPersonModeRef.current
|
||||||
|
wasFirstPersonModeRef.current = isFirstPersonMode
|
||||||
|
|
||||||
|
if (isFirstPersonMode && !wasFirstPersonMode) {
|
||||||
|
const viewer = useViewer.getState()
|
||||||
|
firstPersonPreviousLevelRef.current = viewer.selection.levelId
|
||||||
|
viewer.setCameraMode('perspective')
|
||||||
|
viewer.setWallMode('up')
|
||||||
|
viewer.setWalkthroughMode(true)
|
||||||
|
viewer.setSelection({ selectedIds: [], zoneId: null })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(wasFirstPersonMode && !isFirstPersonMode)) return
|
||||||
|
|
||||||
|
const viewer = useViewer.getState()
|
||||||
|
const previousLevelId = firstPersonPreviousLevelRef.current
|
||||||
|
firstPersonPreviousLevelRef.current = null
|
||||||
|
viewer.setWalkthroughMode(false)
|
||||||
|
|
||||||
|
if (!previousLevelId) return
|
||||||
|
|
||||||
|
const previousLevelNode = useScene.getState().nodes[previousLevelId]
|
||||||
|
if (previousLevelNode?.type === 'level') {
|
||||||
|
viewer.setSelection({
|
||||||
|
levelId: previousLevelId,
|
||||||
|
zoneId: null,
|
||||||
|
selectedIds: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [isFirstPersonMode])
|
||||||
|
|
||||||
const previewViewerContent = (
|
const previewViewerContent = (
|
||||||
<Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
|
<Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
|
||||||
<ExportManager />
|
<ExportManager />
|
||||||
@@ -1135,21 +1141,24 @@ export default function Editor({
|
|||||||
navbarSlot={navbarSlot}
|
navbarSlot={navbarSlot}
|
||||||
overlays={
|
overlays={
|
||||||
<>
|
<>
|
||||||
<FloatingLevelSelector />
|
{!isCaptureMode && <FloatingLevelSelector />}
|
||||||
{!isVersionPreviewMode && (
|
{!(isVersionPreviewMode || isCaptureMode) && (
|
||||||
<div className="pointer-events-auto">
|
<div className="pointer-events-auto">
|
||||||
<ActionMenu />
|
<ActionMenu />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isVersionPreviewMode && (
|
{!(isVersionPreviewMode || isCaptureMode) && (
|
||||||
<div className="pointer-events-auto">
|
<div className="pointer-events-auto">
|
||||||
<PanelManager />
|
<PanelManager />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isCaptureMode && (
|
||||||
<div className="pointer-events-auto">
|
<div className="pointer-events-auto">
|
||||||
<HelperManager />
|
<HelperManager />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{viewerBanner}
|
{viewerBanner}
|
||||||
|
{projectId ? <SnapshotCaptureOverlay projectId={projectId} /> : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
renderTabContent={renderTabContent}
|
renderTabContent={renderTabContent}
|
||||||
@@ -1159,14 +1168,14 @@ export default function Editor({
|
|||||||
viewerToolbarLeft={viewerToolbarLeft}
|
viewerToolbarLeft={viewerToolbarLeft}
|
||||||
viewerToolbarRight={viewerToolbarRight}
|
viewerToolbarRight={viewerToolbarRight}
|
||||||
/>
|
/>
|
||||||
|
<EditorCommands />
|
||||||
|
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
||||||
{/* First-person overlay — rendered on top of normal layout */}
|
{/* First-person overlay — rendered on top of normal layout */}
|
||||||
{isFirstPersonMode && (
|
{isFirstPersonMode && (
|
||||||
<div className="pointer-events-none fixed inset-0 z-50">
|
<div className="pointer-events-none fixed inset-0 z-50">
|
||||||
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<EditorCommands />
|
|
||||||
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PresetsProvider>
|
</PresetsProvider>
|
||||||
@@ -1222,6 +1231,12 @@ export default function Editor({
|
|||||||
<HelperManager />
|
<HelperManager />
|
||||||
</div>
|
</div>
|
||||||
</ViewerOverlays>
|
</ViewerOverlays>
|
||||||
|
{/* First-person overlay — rendered on top of normal layout */}
|
||||||
|
{isFirstPersonMode && (
|
||||||
|
<div className="pointer-events-none fixed inset-0 z-50">
|
||||||
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Executable → Regular
+2
-2
@@ -341,7 +341,7 @@ function applySingleSurfacePaintPreview(
|
|||||||
if (node.type === 'ceiling') {
|
if (node.type === 'ceiling') {
|
||||||
const root = getRegisteredMesh(node.id)
|
const root = getRegisteredMesh(node.id)
|
||||||
const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined
|
const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined
|
||||||
if (!root || !overlay) return null
|
if (!(root && overlay)) return null
|
||||||
|
|
||||||
const previewColor =
|
const previewColor =
|
||||||
getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ??
|
getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ??
|
||||||
@@ -999,7 +999,6 @@ export const SelectionManager = () => {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
'spawn',
|
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
'zone',
|
'zone',
|
||||||
@@ -1392,6 +1391,7 @@ export const SelectionManager = () => {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
|
'spawn',
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
'zone',
|
'zone',
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { emitter } from '@pascal-app/core'
|
||||||
|
import { Camera, Check, Crop, Loader2, Maximize2, Monitor, X } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useIsMobile } from '../../hooks/use-mobile'
|
||||||
|
import { triggerSFX } from '../../lib/sfx-bus'
|
||||||
|
import useEditor from '../../store/use-editor'
|
||||||
|
|
||||||
|
type CaptureMode = 'standard' | 'viewport' | 'area'
|
||||||
|
type CaptureState = 'idle' | 'capturing' | 'saved'
|
||||||
|
|
||||||
|
interface DragPoint {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Drag {
|
||||||
|
start: DragPoint
|
||||||
|
end: DragPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResolution(
|
||||||
|
mode: CaptureMode,
|
||||||
|
overlayEl: HTMLDivElement | null,
|
||||||
|
drag: Drag | null,
|
||||||
|
): { w: number; h: number } | null {
|
||||||
|
if (mode === 'standard') return { w: 1920, h: 1080 }
|
||||||
|
|
||||||
|
if (!overlayEl) return null
|
||||||
|
const rect = overlayEl.getBoundingClientRect()
|
||||||
|
const dpr = Math.min(window.devicePixelRatio, 1.5)
|
||||||
|
|
||||||
|
if (mode === 'viewport') {
|
||||||
|
return { w: Math.round(rect.width * dpr), h: Math.round(rect.height * dpr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'area' && drag) {
|
||||||
|
const w = Math.abs(drag.end.x - drag.start.x)
|
||||||
|
const h = Math.abs(drag.end.y - drag.start.y)
|
||||||
|
if (w < 4 || h < 4) return null
|
||||||
|
return { w: Math.round(w * dpr), h: Math.round(h * dpr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
||||||
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
||||||
|
const setCaptureMode = useEditor((s) => s.setCaptureMode)
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<CaptureMode>('standard')
|
||||||
|
const [drag, setDrag] = useState<Drag | null>(null)
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const [captureState, setCaptureState] = useState<CaptureState>('idle')
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Dismiss on Esc
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCaptureMode) return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setCaptureMode(false)
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [isCaptureMode, setCaptureMode])
|
||||||
|
|
||||||
|
// Reset local state when entering capture mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCaptureMode) {
|
||||||
|
setMode('standard')
|
||||||
|
setDrag(null)
|
||||||
|
setIsDragging(false)
|
||||||
|
setCaptureState('idle')
|
||||||
|
}
|
||||||
|
}, [isCaptureMode])
|
||||||
|
|
||||||
|
// Listen for snapshot saved to show feedback then exit
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
setCaptureState('saved')
|
||||||
|
setTimeout(() => {
|
||||||
|
setCaptureMode(false)
|
||||||
|
setCaptureState('idle')
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
emitter.on('snapshot:saved', handler)
|
||||||
|
return () => emitter.off('snapshot:saved', handler)
|
||||||
|
}, [setCaptureMode])
|
||||||
|
|
||||||
|
const dismiss = useCallback(() => setCaptureMode(false), [setCaptureMode])
|
||||||
|
|
||||||
|
// Tracks whether the active drag is a "move entire rect" gesture
|
||||||
|
const moveStartRef = useRef<{ pt: DragPoint; drag: Drag } | null>(null)
|
||||||
|
|
||||||
|
// Area drag handlers — relative to the overlay container
|
||||||
|
const onPointerDown = useCallback(
|
||||||
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (mode !== 'area' || captureState !== 'idle') return
|
||||||
|
e.preventDefault()
|
||||||
|
const rect = overlayRef.current!.getBoundingClientRect()
|
||||||
|
const pt = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
|
|
||||||
|
// If clicking inside an existing selection → move mode
|
||||||
|
if (drag) {
|
||||||
|
const x0 = Math.min(drag.start.x, drag.end.x)
|
||||||
|
const y0 = Math.min(drag.start.y, drag.end.y)
|
||||||
|
const x1 = Math.max(drag.start.x, drag.end.x)
|
||||||
|
const y1 = Math.max(drag.start.y, drag.end.y)
|
||||||
|
if (pt.x >= x0 && pt.x <= x1 && pt.y >= y0 && pt.y <= y1) {
|
||||||
|
moveStartRef.current = { pt, drag }
|
||||||
|
setIsDragging(true)
|
||||||
|
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outside / no selection → start new drag
|
||||||
|
moveStartRef.current = null
|
||||||
|
setDrag({ start: pt, end: pt })
|
||||||
|
setIsDragging(true)
|
||||||
|
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
||||||
|
},
|
||||||
|
[mode, captureState, drag],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPointerMove = useCallback(
|
||||||
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (!isDragging) return
|
||||||
|
const rect = overlayRef.current!.getBoundingClientRect()
|
||||||
|
const pt = {
|
||||||
|
x: Math.max(0, Math.min(e.clientX - rect.left, rect.width)),
|
||||||
|
y: Math.max(0, Math.min(e.clientY - rect.top, rect.height)),
|
||||||
|
}
|
||||||
|
if (moveStartRef.current) {
|
||||||
|
// Move mode: translate the whole rect by the delta
|
||||||
|
const { pt: origin, drag: snapshot } = moveStartRef.current
|
||||||
|
const dx = pt.x - origin.x
|
||||||
|
const dy = pt.y - origin.y
|
||||||
|
setDrag({
|
||||||
|
start: { x: snapshot.start.x + dx, y: snapshot.start.y + dy },
|
||||||
|
end: { x: snapshot.end.x + dx, y: snapshot.end.y + dy },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setDrag((d) => (d ? { start: d.start, end: pt } : null))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDragging],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPointerUp = useCallback(() => {
|
||||||
|
const wasMoving = moveStartRef.current !== null
|
||||||
|
setIsDragging(false)
|
||||||
|
moveStartRef.current = null
|
||||||
|
// Clear the rect if the user just clicked without drawing (not a move gesture)
|
||||||
|
if (!wasMoving) {
|
||||||
|
setDrag((d) => {
|
||||||
|
if (!d) return null
|
||||||
|
const w = Math.abs(d.end.x - d.start.x)
|
||||||
|
const h = Math.abs(d.end.y - d.start.y)
|
||||||
|
return w < 4 && h < 4 ? null : d
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Corner-handle resize: re-anchor to the opposite corner then reuse the same drag machinery
|
||||||
|
const onCornerPointerDown = useCallback(
|
||||||
|
(e: React.PointerEvent<HTMLDivElement>, cornerIndex: number) => {
|
||||||
|
if (captureState !== 'idle' || !drag) return
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
moveStartRef.current = null
|
||||||
|
const x0 = Math.min(drag.start.x, drag.end.x)
|
||||||
|
const y0 = Math.min(drag.start.y, drag.end.y)
|
||||||
|
const x1 = Math.max(drag.start.x, drag.end.x)
|
||||||
|
const y1 = Math.max(drag.start.y, drag.end.y)
|
||||||
|
// anchor = opposite corner; dragged = current corner
|
||||||
|
const corners: [DragPoint, DragPoint][] = [
|
||||||
|
[
|
||||||
|
{ x: x1, y: y1 },
|
||||||
|
{ x: x0, y: y0 },
|
||||||
|
], // TL → anchor BR
|
||||||
|
[
|
||||||
|
{ x: x0, y: y1 },
|
||||||
|
{ x: x1, y: y0 },
|
||||||
|
], // TR → anchor BL
|
||||||
|
[
|
||||||
|
{ x: x1, y: y0 },
|
||||||
|
{ x: x0, y: y1 },
|
||||||
|
], // BL → anchor TR
|
||||||
|
[
|
||||||
|
{ x: x0, y: y0 },
|
||||||
|
{ x: x1, y: y1 },
|
||||||
|
], // BR → anchor TL
|
||||||
|
]
|
||||||
|
const [anchor, current] = corners[cornerIndex]!
|
||||||
|
setDrag({ start: anchor, end: current })
|
||||||
|
setIsDragging(true)
|
||||||
|
},
|
||||||
|
[captureState, drag],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCapture = useCallback(() => {
|
||||||
|
if (captureState !== 'idle') return
|
||||||
|
|
||||||
|
let cropRegion: { x: number; y: number; width: number; height: number } | undefined
|
||||||
|
if (mode === 'area' && drag && overlayRef.current) {
|
||||||
|
const rect = overlayRef.current.getBoundingClientRect()
|
||||||
|
const x0 = Math.min(drag.start.x, drag.end.x)
|
||||||
|
const y0 = Math.min(drag.start.y, drag.end.y)
|
||||||
|
const w = Math.abs(drag.end.x - drag.start.x)
|
||||||
|
const h = Math.abs(drag.end.y - drag.start.y)
|
||||||
|
cropRegion = {
|
||||||
|
x: x0 / rect.width,
|
||||||
|
y: y0 / rect.height,
|
||||||
|
width: w / rect.width,
|
||||||
|
height: h / rect.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCaptureState('capturing')
|
||||||
|
triggerSFX('sfx:snapshot-capture')
|
||||||
|
emitter.emit('camera-controls:generate-thumbnail', {
|
||||||
|
projectId,
|
||||||
|
captureMode: mode,
|
||||||
|
cropRegion,
|
||||||
|
})
|
||||||
|
}, [captureState, mode, drag, projectId])
|
||||||
|
|
||||||
|
if (!isCaptureMode) return null
|
||||||
|
|
||||||
|
const resolution = getResolution(mode, overlayRef.current, drag)
|
||||||
|
|
||||||
|
// Area selection rect (CSS px, relative to overlay)
|
||||||
|
const selectionStyle =
|
||||||
|
mode === 'area' && drag
|
||||||
|
? {
|
||||||
|
left: Math.min(drag.start.x, drag.end.x),
|
||||||
|
top: Math.min(drag.start.y, drag.end.y),
|
||||||
|
width: Math.abs(drag.end.x - drag.start.x),
|
||||||
|
height: Math.abs(drag.end.y - drag.start.y),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
|
const hasSelection =
|
||||||
|
selectionStyle != null && selectionStyle.width > 3 && selectionStyle.height > 3
|
||||||
|
|
||||||
|
const captureDisabled = captureState !== 'idle' || (mode === 'area' && !hasSelection)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute inset-0 z-40" ref={overlayRef}>
|
||||||
|
{/* Area mode: dim layer + crosshair cursor + drag-to-select */}
|
||||||
|
{mode === 'area' && (
|
||||||
|
<div
|
||||||
|
className="pointer-events-auto absolute inset-0 bg-black/30"
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={(e) => {
|
||||||
|
onPointerMove(e)
|
||||||
|
// Update cursor: 'move' when hovering inside an existing selection
|
||||||
|
if (!isDragging && drag && overlayRef.current) {
|
||||||
|
const rect = overlayRef.current.getBoundingClientRect()
|
||||||
|
const px = e.clientX - rect.left
|
||||||
|
const py = e.clientY - rect.top
|
||||||
|
const x0 = Math.min(drag.start.x, drag.end.x)
|
||||||
|
const y0 = Math.min(drag.start.y, drag.end.y)
|
||||||
|
const x1 = Math.max(drag.start.x, drag.end.x)
|
||||||
|
const y1 = Math.max(drag.start.y, drag.end.y)
|
||||||
|
e.currentTarget.style.cursor =
|
||||||
|
px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
style={{ cursor: 'crosshair' }}
|
||||||
|
>
|
||||||
|
{/* "No selection" hint */}
|
||||||
|
{!selectionStyle && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="rounded-full bg-black/40 px-4 py-2 text-sm text-white backdrop-blur-sm">
|
||||||
|
Drag the area you want to capture
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selection rect */}
|
||||||
|
{selectionStyle && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: selectionStyle.left,
|
||||||
|
top: selectionStyle.top,
|
||||||
|
width: selectionStyle.width,
|
||||||
|
height: selectionStyle.height,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
boxShadow: '0 0 0 9999px rgba(0,0,0,0.35)',
|
||||||
|
border: '2px dashed rgba(255,255,255,0.85)',
|
||||||
|
background: 'rgba(255,255,255,0.04)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Corner handles */}
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ pos: { top: -5, left: -5 }, cursor: 'nwse-resize' },
|
||||||
|
{ pos: { top: -5, right: -5 }, cursor: 'nesw-resize' },
|
||||||
|
{ pos: { bottom: -5, left: -5 }, cursor: 'nesw-resize' },
|
||||||
|
{ pos: { bottom: -5, right: -5 }, cursor: 'nwse-resize' },
|
||||||
|
] as const
|
||||||
|
).map(({ pos, cursor }, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
onPointerDown={(e) => onCornerPointerDown(e, i)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'white',
|
||||||
|
boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
cursor,
|
||||||
|
...pos,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Top-right dismiss button (icon-only on mobile) */}
|
||||||
|
<div className="pointer-events-auto absolute top-4 right-4">
|
||||||
|
<button
|
||||||
|
aria-label="Close capture mode"
|
||||||
|
className="flex items-center gap-1.5 rounded-full border border-white/20 bg-black/60 px-3 py-1.5 text-white/80 text-xs backdrop-blur-sm transition-colors hover:bg-black/80 hover:text-white"
|
||||||
|
onClick={dismiss}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
{!isMobile && 'Esc to cancel'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom-center mode toolbar */}
|
||||||
|
<div className="pointer-events-auto absolute bottom-6 left-1/2 -translate-x-1/2">
|
||||||
|
{(() => {
|
||||||
|
const modeButtons = (
|
||||||
|
<>
|
||||||
|
<ModeButton
|
||||||
|
active={mode === 'standard'}
|
||||||
|
badge="16:9"
|
||||||
|
icon={<Monitor className="h-3.5 w-3.5" />}
|
||||||
|
label="Standard"
|
||||||
|
onClick={() => {
|
||||||
|
setMode('standard')
|
||||||
|
setDrag(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ModeButton
|
||||||
|
active={mode === 'viewport'}
|
||||||
|
icon={<Maximize2 className="h-3.5 w-3.5" />}
|
||||||
|
label="Viewport"
|
||||||
|
onClick={() => {
|
||||||
|
setMode('viewport')
|
||||||
|
setDrag(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ModeButton
|
||||||
|
active={mode === 'area'}
|
||||||
|
icon={<Crop className="h-3.5 w-3.5" />}
|
||||||
|
label="Area"
|
||||||
|
onClick={() => setMode('area')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolutionDisplay = (
|
||||||
|
<span className="min-w-[80px] text-center text-white/50 text-xs tabular-nums">
|
||||||
|
{resolution ? `${resolution.w} × ${resolution.h}` : '—'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
const captureButton = (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-1.5 font-medium text-primary-foreground text-xs transition-opacity disabled:opacity-50"
|
||||||
|
disabled={captureDisabled}
|
||||||
|
onClick={handleCapture}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{captureState === 'capturing' ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
Capturing
|
||||||
|
</>
|
||||||
|
) : captureState === 'saved' ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
Saved
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Camera className="h-3.5 w-3.5" />
|
||||||
|
Capture
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-stretch gap-2 rounded-2xl border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
|
||||||
|
<div className="flex items-center justify-center gap-1">{modeButtons}</div>
|
||||||
|
<div className="flex items-center justify-center gap-2 border-white/10 border-t pt-2">
|
||||||
|
{resolutionDisplay}
|
||||||
|
{captureButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 rounded-full border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
|
||||||
|
{modeButtons}
|
||||||
|
<div className="mx-1 h-4 w-px bg-white/10" />
|
||||||
|
{resolutionDisplay}
|
||||||
|
<div className="mx-1 h-4 w-px bg-white/10" />
|
||||||
|
{captureButton}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModeButton({
|
||||||
|
active,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
badge,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
active: boolean
|
||||||
|
icon: React.ReactNode
|
||||||
|
label: string
|
||||||
|
badge?: string
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs transition-colors ${
|
||||||
|
active ? 'bg-white/15 text-white ring-1 ring-white/20' : 'text-white/50 hover:text-white/90'
|
||||||
|
}`}
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
{badge && (
|
||||||
|
<span className="rounded-sm bg-white/10 px-1 py-0.5 font-medium text-[10px] text-white/40 leading-none">
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { emitter, sceneRegistry, useScene } from '@pascal-app/core'
|
import { emitter, sceneRegistry } from '@pascal-app/core'
|
||||||
import { SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
|
import { SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
|
||||||
import type { CameraControls } from '@react-three/drei'
|
import type { CameraControls } from '@react-three/drei'
|
||||||
import { useThree } from '@react-three/fiber'
|
import { useThree } from '@react-three/fiber'
|
||||||
@@ -28,7 +28,6 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|||||||
|
|
||||||
const THUMBNAIL_WIDTH = 1920
|
const THUMBNAIL_WIDTH = 1920
|
||||||
const THUMBNAIL_HEIGHT = 1080
|
const THUMBNAIL_HEIGHT = 1080
|
||||||
const AUTO_SAVE_DELAY = 10_000
|
|
||||||
|
|
||||||
export interface SnapshotCameraData {
|
export interface SnapshotCameraData {
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
@@ -49,8 +48,6 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
const mainCamera = useThree((state) => state.camera)
|
const mainCamera = useThree((state) => state.camera)
|
||||||
const controls = useThree((state) => state.controls) as CameraControls | null
|
const controls = useThree((state) => state.controls) as CameraControls | null
|
||||||
const isGenerating = useRef(false)
|
const isGenerating = useRef(false)
|
||||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
||||||
const pendingAutoRef = useRef(false)
|
|
||||||
const onThumbnailCaptureRef = useRef(onThumbnailCapture)
|
const onThumbnailCaptureRef = useRef(onThumbnailCapture)
|
||||||
|
|
||||||
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
||||||
@@ -61,7 +58,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
onThumbnailCaptureRef.current = onThumbnailCapture
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
||||||
}, [onThumbnailCapture])
|
}, [onThumbnailCapture])
|
||||||
|
|
||||||
// Build the thumbnail camera, SSGI pipeline, and render target once, reused on every capture.
|
// Build the thumbnail camera, SSGI pipeline, and render target once — reused on every capture.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000)
|
const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000)
|
||||||
cam.layers.disable(EDITOR_LAYER)
|
cam.layers.disable(EDITOR_LAYER)
|
||||||
@@ -75,7 +72,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
|
|
||||||
// pass() handles MRT internally for all material types, including custom
|
// pass() handles MRT internally for all material types, including custom
|
||||||
// shaders, unlike renderer.setMRT() which crashes on non-NodeMaterials.
|
// shaders — unlike renderer.setMRT() which crashes on non-NodeMaterials.
|
||||||
// pass() also respects camera.layers, so EDITOR_LAYER objects are filtered.
|
// pass() also respects camera.layers, so EDITOR_LAYER objects are filtered.
|
||||||
const scenePass = pass(scene, cam)
|
const scenePass = pass(scene, cam)
|
||||||
scenePass.setMRT(
|
scenePass.setMRT(
|
||||||
@@ -117,15 +114,15 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
const ao = (denoisePass as any).r
|
const ao = (denoisePass as any).r
|
||||||
const finalOutput = vec4(scenePassColor.rgb.mul(ao), scenePassColor.a)
|
const finalOutput = vec4(scenePassColor.rgb.mul(ao), scenePassColor.a)
|
||||||
|
|
||||||
// FXAA requires a texture node as input, convertToTexture renders finalOutput
|
// FXAA requires a texture node as input; convertToTexture renders finalOutput
|
||||||
// into an intermediate RT so FXAA can sample it with neighbor UV offsets.
|
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
|
||||||
const aaOutput = fxaa(convertToTexture(finalOutput))
|
const aaOutput = fxaa(convertToTexture(finalOutput))
|
||||||
|
|
||||||
const pipeline = new RenderPipeline(gl as unknown as WebGPURenderer)
|
const pipeline = new RenderPipeline(gl as unknown as WebGPURenderer)
|
||||||
pipeline.outputNode = aaOutput
|
pipeline.outputNode = aaOutput
|
||||||
pipelineRef.current = pipeline
|
pipelineRef.current = pipeline
|
||||||
|
|
||||||
// Dedicated render target, pipeline outputs here instead of the canvas,
|
// Dedicated render target — pipeline outputs here instead of the canvas,
|
||||||
// so R3F's main render loop can never overwrite our capture.
|
// so R3F's main render loop can never overwrite our capture.
|
||||||
const { width, height } = gl.domElement
|
const { width, height } = gl.domElement
|
||||||
renderTargetRef.current = new RenderTarget(width, height, { depthBuffer: true })
|
renderTargetRef.current = new RenderTarget(width, height, { depthBuffer: true })
|
||||||
@@ -176,7 +173,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
thumbnailCamera.aspect = width / height
|
thumbnailCamera.aspect = width / height
|
||||||
thumbnailCamera.updateProjectionMatrix()
|
thumbnailCamera.updateProjectionMatrix()
|
||||||
|
|
||||||
// Capture camera data for snapshot storage.
|
// Capture camera data for snapshot storage
|
||||||
const pos = mainCamera.position
|
const pos = mainCamera.position
|
||||||
let tgt: [number, number, number] | null = null
|
let tgt: [number, number, number] | null = null
|
||||||
if (controls && 'getTarget' in controls) {
|
if (controls && 'getTarget' in controls) {
|
||||||
@@ -192,7 +189,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
||||||
}
|
}
|
||||||
|
|
||||||
// For auto-save, snap levels to stacked positions and reset levelMode.
|
// For auto-save: snap levels to stacked positions and reset levelMode
|
||||||
let restoreLevelMode: (() => void) | null = null
|
let restoreLevelMode: (() => void) | null = null
|
||||||
let restoreLevels: () => void = () => {}
|
let restoreLevels: () => void = () => {}
|
||||||
if (snapLevels) {
|
if (snapLevels) {
|
||||||
@@ -205,7 +202,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide scan and guide nodes directly so they are excluded from the
|
// Hide scan and guide nodes directly so they are excluded from the
|
||||||
// thumbnail regardless of whether ScanSystem or GuideSystem listeners are
|
// thumbnail regardless of whether ScanSystem/GuideSystem listeners are
|
||||||
// registered. Returns a function that restores the original visibility.
|
// registered. Returns a function that restores the original visibility.
|
||||||
const restoreNodeVisibility = (() => {
|
const restoreNodeVisibility = (() => {
|
||||||
const saved = new Map<THREE.Object3D, boolean>()
|
const saved = new Map<THREE.Object3D, boolean>()
|
||||||
@@ -231,7 +228,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
if (pipelineRef.current && renderTargetRef.current) {
|
if (pipelineRef.current && renderTargetRef.current) {
|
||||||
const rt = renderTargetRef.current
|
const rt = renderTargetRef.current
|
||||||
|
|
||||||
// Resize RT if the canvas dimensions changed.
|
// Resize RT if the canvas dimensions changed
|
||||||
if (rt.width !== width || rt.height !== height) {
|
if (rt.width !== width || rt.height !== height) {
|
||||||
rt.setSize(width, height)
|
rt.setSize(width, height)
|
||||||
}
|
}
|
||||||
@@ -248,7 +245,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
emitter.emit('thumbnail:after-capture', undefined)
|
emitter.emit('thumbnail:after-capture', undefined)
|
||||||
|
|
||||||
// Restore level positions, levelMode, and node visibility immediately after the
|
// Restore level positions, levelMode, and node visibility immediately after the
|
||||||
// render, before the async GPU readback.
|
// render — before the async GPU readback.
|
||||||
restoreLevels()
|
restoreLevels()
|
||||||
restoreLevelMode?.()
|
restoreLevelMode?.()
|
||||||
restoreNodeVisibility()
|
restoreNodeVisibility()
|
||||||
@@ -339,6 +336,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH)
|
offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH)
|
||||||
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
||||||
} else {
|
} else {
|
||||||
|
// Standard: center-crop to 1920×1080 aspect ratio
|
||||||
const srcAspect = width / height
|
const srcAspect = width / height
|
||||||
const dstAspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT
|
const dstAspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT
|
||||||
let sx = 0,
|
let sx = 0,
|
||||||
@@ -364,7 +362,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
||||||
cameraData.resolution = { w: outW, h: outH }
|
cameraData.resolution = { w: outW, h: outH }
|
||||||
} else {
|
} else {
|
||||||
// Fallback: plain render directly to the canvas.
|
// Fallback: plain render directly to the canvas
|
||||||
emitter.emit('thumbnail:before-capture', undefined)
|
emitter.emit('thumbnail:before-capture', undefined)
|
||||||
gl.render(scene, thumbnailCamera)
|
gl.render(scene, thumbnailCamera)
|
||||||
emitter.emit('thumbnail:after-capture', undefined)
|
emitter.emit('thumbnail:after-capture', undefined)
|
||||||
@@ -450,10 +448,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Thumbnail request via emitter. Two call shapes:
|
// Thumbnail request via emitter. Two call shapes:
|
||||||
// - user-driven capture: `{ projectId, captureMode, cropRegion }`, captures
|
// - user-driven capture: `{ projectId, captureMode, cropRegion }` — captures
|
||||||
// the current pose with the supplied crop.
|
// the current pose with the supplied crop.
|
||||||
// - auto-save capture: `{ projectId, snapLevels: true }`, snaps levels to
|
// - host-driven auto-save: `{ projectId, snapLevels: true }` — snaps levels
|
||||||
// their true positions first for a consistent auto-thumbnail angle.
|
// to their true positions first for a consistent auto-thumbnail angle.
|
||||||
|
// The caller owns policy (when to fire, whether the tab is visible).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onThumbnailCapture) return
|
if (!onThumbnailCapture) return
|
||||||
|
|
||||||
@@ -469,49 +468,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|||||||
return () => emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
return () => emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
||||||
}, [generate, onThumbnailCapture])
|
}, [generate, onThumbnailCapture])
|
||||||
|
|
||||||
// OSS adaptation: keep local debounced auto-capture behavior because the
|
// Go-to-camera: animate camera to a saved snapshot position/target
|
||||||
// community host-side autosave hook is not part of this repo.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!onThumbnailCapture) return
|
|
||||||
|
|
||||||
const triggerNow = () => {
|
|
||||||
void generate(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleOrDefer = () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
triggerNow()
|
|
||||||
} else {
|
|
||||||
pendingAutoRef.current = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSceneChange = () => {
|
|
||||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
||||||
debounceTimerRef.current = setTimeout(scheduleOrDefer, AUTO_SAVE_DELAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
if (document.visibilityState === 'visible' && pendingAutoRef.current) {
|
|
||||||
pendingAutoRef.current = false
|
|
||||||
triggerNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribe = useScene.subscribe((state, prevState) => {
|
|
||||||
if (state.nodes !== prevState.nodes) onSceneChange()
|
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
||||||
unsubscribe()
|
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
||||||
}
|
|
||||||
}, [generate, onThumbnailCapture])
|
|
||||||
|
|
||||||
// Go-to-camera: animate camera to a saved snapshot position or target.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = ({
|
const handler = ({
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -144,11 +144,11 @@ export function useFloorplanBackgroundPlacement({
|
|||||||
emitFloorplanGridEvent('click', snappedPoint, event)
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
||||||
setCursorPoint(snappedPoint)
|
setCursorPoint(snappedPoint)
|
||||||
|
|
||||||
if (!roofDraftStart) {
|
if (roofDraftStart) {
|
||||||
|
clearRoofPlacementDraft()
|
||||||
|
} else {
|
||||||
setRoofDraftStart(snappedPoint)
|
setRoofDraftStart(snappedPoint)
|
||||||
setRoofDraftEnd(snappedPoint)
|
setRoofDraftEnd(snappedPoint)
|
||||||
} else {
|
|
||||||
clearRoofPlacementDraft()
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
@@ -1,265 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useScene } from '@pascal-app/core'
|
|
||||||
import { ImageIcon, MessageSquare, X } from 'lucide-react'
|
|
||||||
import { useCallback, useRef, useState } from 'react'
|
|
||||||
import { Button } from './ui/primitives/button'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from './ui/primitives/dialog'
|
|
||||||
|
|
||||||
const MAX_IMAGES = 5
|
|
||||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024
|
|
||||||
|
|
||||||
type ImagePreview = { file: File; url: string }
|
|
||||||
|
|
||||||
export function FeedbackDialog({
|
|
||||||
projectId: projectIdProp,
|
|
||||||
onSubmit,
|
|
||||||
}: {
|
|
||||||
projectId?: string
|
|
||||||
onSubmit?: (data: {
|
|
||||||
message: string
|
|
||||||
projectId?: string
|
|
||||||
sceneGraph: unknown
|
|
||||||
images: File[]
|
|
||||||
}) => Promise<{ success: boolean; error?: string }>
|
|
||||||
}) {
|
|
||||||
const projectId = projectIdProp
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [message, setMessage] = useState('')
|
|
||||||
const [images, setImages] = useState<ImagePreview[]>([])
|
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [sent, setSent] = useState(false)
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const dragCounter = useRef(0)
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
|
||||||
setOpen(true)
|
|
||||||
setSent(false)
|
|
||||||
setError(null)
|
|
||||||
setMessage('')
|
|
||||||
setImages([])
|
|
||||||
setIsDragging(false)
|
|
||||||
dragCounter.current = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (isSubmitting) return
|
|
||||||
setOpen(false)
|
|
||||||
images.forEach((img) => {
|
|
||||||
URL.revokeObjectURL(img.url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addFiles = useCallback((files: FileList | File[]) => {
|
|
||||||
const incoming = Array.from(files).filter(
|
|
||||||
(f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE,
|
|
||||||
)
|
|
||||||
setImages((prev) => {
|
|
||||||
const remaining = MAX_IMAGES - prev.length
|
|
||||||
const added = incoming.slice(0, remaining).map((file) => ({
|
|
||||||
file,
|
|
||||||
url: URL.createObjectURL(file),
|
|
||||||
}))
|
|
||||||
return [...prev, ...added]
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const removeImage = (index: number) => {
|
|
||||||
setImages((prev) => {
|
|
||||||
const img = prev[index]
|
|
||||||
if (img) URL.revokeObjectURL(img.url)
|
|
||||||
return prev.filter((_, i) => i !== index)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Drag handlers (on the entire dialog content) ──
|
|
||||||
const onDragEnter = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
dragCounter.current++
|
|
||||||
if (e.dataTransfer.types.includes('Files')) {
|
|
||||||
setIsDragging(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDragLeave = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
dragCounter.current--
|
|
||||||
if (dragCounter.current === 0) {
|
|
||||||
setIsDragging(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDragOver = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
dragCounter.current = 0
|
|
||||||
setIsDragging(false)
|
|
||||||
if (e.dataTransfer.files.length > 0) {
|
|
||||||
addFiles(e.dataTransfer.files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setError(null)
|
|
||||||
setIsSubmitting(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!onSubmit) return
|
|
||||||
const { nodes, rootNodeIds } = useScene.getState()
|
|
||||||
const sceneGraph = { nodes, rootNodeIds }
|
|
||||||
const result = await onSubmit({
|
|
||||||
message,
|
|
||||||
projectId,
|
|
||||||
sceneGraph,
|
|
||||||
images: images.map((img) => img.file),
|
|
||||||
})
|
|
||||||
if (result.success) {
|
|
||||||
setSent(true)
|
|
||||||
setTimeout(() => setOpen(false), 1500)
|
|
||||||
} else {
|
|
||||||
setError(result.error ?? 'Something went wrong')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="flex items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent/90"
|
|
||||||
onClick={handleOpen}
|
|
||||||
>
|
|
||||||
<MessageSquare className="h-4 w-4" />
|
|
||||||
Feedback
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Dialog onOpenChange={handleClose} open={open}>
|
|
||||||
<DialogContent
|
|
||||||
className="sm:max-w-[460px]"
|
|
||||||
onDragEnter={onDragEnter}
|
|
||||||
onDragLeave={onDragLeave}
|
|
||||||
onDragOver={onDragOver}
|
|
||||||
onDrop={onDrop}
|
|
||||||
>
|
|
||||||
{/* Drag overlay — only visible when dragging files over the dialog */}
|
|
||||||
{isDragging && (
|
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg border-2 border-primary/50 border-dashed bg-primary/5 backdrop-blur-sm transition-all">
|
|
||||||
<div className="flex flex-col items-center gap-2 text-primary/70">
|
|
||||||
<ImageIcon className="h-8 w-8" />
|
|
||||||
<p className="font-medium text-sm">Drop images here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Send Feedback</DialogTitle>
|
|
||||||
<DialogDescription>We'd love to hear your thoughts</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{sent ? (
|
|
||||||
<p className="py-4 text-center text-muted-foreground text-sm">
|
|
||||||
Thanks for your feedback!
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<label className="font-medium text-sm" htmlFor="feedback-message">
|
|
||||||
Your feedback
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
autoFocus
|
|
||||||
className="mt-1 w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
id="feedback-message"
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
placeholder="Share your thoughts, suggestions, feature requests, or report issues..."
|
|
||||||
rows={5}
|
|
||||||
value={message}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image thumbnails */}
|
|
||||||
{images.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{images.map((img, i) => (
|
|
||||||
<div
|
|
||||||
className="group relative h-14 w-14 overflow-hidden rounded-md border border-border"
|
|
||||||
key={img.url}
|
|
||||||
>
|
|
||||||
<img alt="" className="h-full w-full object-cover" src={img.url} />
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
onClick={() => removeImage(i)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 text-white" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && <p className="text-destructive text-sm">{error}</p>}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{/* Subtle attach button */}
|
|
||||||
<button
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground text-xs transition-colors hover:text-foreground disabled:opacity-40"
|
|
||||||
disabled={isSubmitting || images.length >= MAX_IMAGES}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<ImageIcon className="h-3.5 w-3.5" />
|
|
||||||
{images.length > 0 ? `${images.length}/${MAX_IMAGES}` : 'Attach'}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
multiple
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.files) addFiles(e.target.files)
|
|
||||||
e.target.value = ''
|
|
||||||
}}
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={handleClose}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button disabled={isSubmitting || !message.trim() || !onSubmit} type="submit">
|
|
||||||
{isSubmitting ? 'Sending...' : 'Send Feedback'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Howl } from 'howler'
|
|
||||||
import { Disc3, Settings2, SkipBack, SkipForward, Volume2, VolumeX } from 'lucide-react'
|
|
||||||
import { AnimatePresence, motion } from 'motion/react'
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { Slider } from '../components/ui/slider'
|
|
||||||
import { cn } from '../lib/utils'
|
|
||||||
import useAudio from '../store/use-audio'
|
|
||||||
|
|
||||||
const PLAYLIST = [
|
|
||||||
{
|
|
||||||
title: 'Ballroom in Miniature',
|
|
||||||
file: '/audios/radios/classic/Ballroom in Miniature.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Blueprints in Springtime',
|
|
||||||
file: '/audios/radios/classic/Blueprints in Springtime.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Clockwork Tea Party',
|
|
||||||
file: '/audios/radios/classic/Clockwork Tea Party.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Clockwork Tea Party (Alternate)',
|
|
||||||
file: '/audios/radios/classic/Clockwork Tea Party (Alternate).mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Clockwork Teacups',
|
|
||||||
file: '/audios/radios/classic/Clockwork Teacups.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Evening in the Parlor',
|
|
||||||
file: '/audios/radios/classic/Evening in the Parlor.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Glass Atrium',
|
|
||||||
file: '/audios/radios/classic/Glass Atrium.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Moonlight On The Drafting Table',
|
|
||||||
file: '/audios/radios/classic/Moonlight On The Drafting Table.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Sunlit Garden Reverie',
|
|
||||||
file: '/audios/radios/classic/Sunlit Garden Reverie.mp3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Sunlit Waltz in Pastel Hues',
|
|
||||||
file: '/audios/radios/classic/Sunlit Waltz in Pastel Hues.mp3',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Shuffle array helper
|
|
||||||
function shuffleArray<T>(array: T[]): T[] {
|
|
||||||
const shuffled = [...array]
|
|
||||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1))
|
|
||||||
;[shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!]
|
|
||||||
}
|
|
||||||
return shuffled
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PascalRadio() {
|
|
||||||
const [shuffledPlaylist] = useState(() => shuffleArray(PLAYLIST))
|
|
||||||
const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
|
|
||||||
const { masterVolume, radioVolume, muted, isRadioPlaying, setRadioPlaying } = useAudio()
|
|
||||||
const soundRef = useRef<Howl | null>(null)
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const currentTrack = shuffledPlaylist[currentTrackIndex]!
|
|
||||||
|
|
||||||
// Calculate effective volume (masterVolume * radioVolume, both are 0-100)
|
|
||||||
const effectiveVolume = (masterVolume / 100) * (radioVolume / 100)
|
|
||||||
|
|
||||||
// Keep a ref so the track-init effect can read current volume/muted/isRadioPlaying
|
|
||||||
// without those values being part of its dependency array (which would restart the song).
|
|
||||||
const effectiveVolumeRef = useRef(effectiveVolume)
|
|
||||||
const mutedRef = useRef(muted)
|
|
||||||
const isPlayingRef = useRef(isRadioPlaying)
|
|
||||||
effectiveVolumeRef.current = effectiveVolume
|
|
||||||
mutedRef.current = muted
|
|
||||||
isPlayingRef.current = isRadioPlaying
|
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
|
||||||
setCurrentTrackIndex((prev) => (prev + 1) % shuffledPlaylist.length)
|
|
||||||
}, [shuffledPlaylist.length])
|
|
||||||
|
|
||||||
const handlePrevious = useCallback(() => {
|
|
||||||
setCurrentTrackIndex((prev) => (prev - 1 + shuffledPlaylist.length) % shuffledPlaylist.length)
|
|
||||||
}, [shuffledPlaylist.length])
|
|
||||||
|
|
||||||
// Initialize Howler only when the track changes — not on volume/mute/play-state changes.
|
|
||||||
// Volume and mute are handled by the separate effect below.
|
|
||||||
useEffect(() => {
|
|
||||||
if (soundRef.current) {
|
|
||||||
soundRef.current.unload()
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasPlaying = isPlayingRef.current
|
|
||||||
|
|
||||||
soundRef.current = new Howl({
|
|
||||||
src: [currentTrack.file],
|
|
||||||
volume: mutedRef.current ? 0 : effectiveVolumeRef.current,
|
|
||||||
onend: handleNext,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (wasPlaying && !mutedRef.current) {
|
|
||||||
soundRef.current?.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
soundRef.current?.unload()
|
|
||||||
}
|
|
||||||
}, [handleNext, currentTrack.file])
|
|
||||||
|
|
||||||
// Update volume when settings change
|
|
||||||
useEffect(() => {
|
|
||||||
if (soundRef.current) {
|
|
||||||
soundRef.current.volume(muted ? 0 : effectiveVolume)
|
|
||||||
|
|
||||||
// Pause if muted, resume if unmuted and was playing
|
|
||||||
if (muted && isRadioPlaying) {
|
|
||||||
soundRef.current.pause()
|
|
||||||
} else if (!muted && isRadioPlaying && !soundRef.current.playing()) {
|
|
||||||
soundRef.current.play()
|
|
||||||
} else if (!isRadioPlaying && soundRef.current.playing()) {
|
|
||||||
soundRef.current.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [effectiveVolume, muted, isRadioPlaying])
|
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
|
||||||
if (!soundRef.current || muted) return
|
|
||||||
|
|
||||||
if (isRadioPlaying) {
|
|
||||||
soundRef.current.pause()
|
|
||||||
} else {
|
|
||||||
soundRef.current.play()
|
|
||||||
}
|
|
||||||
setRadioPlaying(!isRadioPlaying)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVolumeChange = (value: number[]) => {
|
|
||||||
useAudio.setState({ radioVolume: value[0] })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle click outside to close
|
|
||||||
useEffect(() => {
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
|
||||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
||||||
setIsOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col overflow-hidden rounded-lg border border-border bg-background/95 shadow-lg backdrop-blur-md',
|
|
||||||
!isOpen && 'cursor-pointer transition-colors hover:bg-accent/90',
|
|
||||||
)}
|
|
||||||
layout
|
|
||||||
onClick={() => {
|
|
||||||
if (!isOpen) setIsOpen(true)
|
|
||||||
}}
|
|
||||||
ref={containerRef}
|
|
||||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2 px-3 py-2 font-medium text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Disc3 className={cn('h-4 w-4 shrink-0', isRadioPlaying && 'animate-spin')} />
|
|
||||||
<span className="hidden whitespace-nowrap sm:inline">Radio Pascal</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
aria-label={isRadioPlaying ? 'Pause' : 'Play'}
|
|
||||||
className="cursor-pointer rounded-sm bg-accent/30 p-1 transition-all hover:bg-accent hover:text-accent-foreground hover:shadow-sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handlePlayPause()
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
handlePlayPause()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{isRadioPlaying ? (
|
|
||||||
<Volume2 className="h-3.5 w-3.5" />
|
|
||||||
) : (
|
|
||||||
<VolumeX className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
aria-label="Radio Settings"
|
|
||||||
className={cn(
|
|
||||||
'cursor-pointer rounded-sm p-1 transition-all hover:bg-accent hover:text-accent-foreground',
|
|
||||||
isOpen && 'bg-accent text-accent-foreground',
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setIsOpen(!isOpen)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings2 className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
|
|
||||||
>
|
|
||||||
<div className="w-[16rem] space-y-3 px-3 pb-3">
|
|
||||||
<div className="mb-3 h-px w-full bg-border/50" />
|
|
||||||
{/* Current song info with prev/next */}
|
|
||||||
<div>
|
|
||||||
<p className="mb-2 text-muted-foreground text-xs">Now Playing</p>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<button
|
|
||||||
aria-label="Previous"
|
|
||||||
className="shrink-0 rounded-full p-1.5 transition-colors hover:bg-accent"
|
|
||||||
onClick={handlePrevious}
|
|
||||||
>
|
|
||||||
<SkipBack className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<p
|
|
||||||
className="flex-1 truncate text-center font-medium text-sm"
|
|
||||||
title={currentTrack.title}
|
|
||||||
>
|
|
||||||
{currentTrack.title}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
aria-label="Next"
|
|
||||||
className="shrink-0 rounded-full p-1.5 transition-colors hover:bg-accent"
|
|
||||||
onClick={handleNext}
|
|
||||||
>
|
|
||||||
<SkipForward className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Volume control */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Volume2 className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
||||||
<Slider
|
|
||||||
aria-label="Radio Volume"
|
|
||||||
className="flex-1"
|
|
||||||
max={100}
|
|
||||||
onValueChange={handleVolumeChange}
|
|
||||||
step={1}
|
|
||||||
value={[radioVolume]}
|
|
||||||
/>
|
|
||||||
<span className="w-8 shrink-0 text-right text-muted-foreground text-xs">
|
|
||||||
{radioVolume}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Eye } from 'lucide-react'
|
|
||||||
import useEditor from '../store/use-editor'
|
|
||||||
|
|
||||||
export function PreviewButton() {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent/90"
|
|
||||||
onClick={() => useEditor.getState().setPreviewMode(true)}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 shrink-0" />
|
|
||||||
<span className="hidden whitespace-nowrap sm:inline">Preview</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+10
-12
@@ -75,7 +75,9 @@ const CeilingSelectionAffordance = ({
|
|||||||
ceiling: CeilingNode
|
ceiling: CeilingNode
|
||||||
levelId: string
|
levelId: string
|
||||||
}) => {
|
}) => {
|
||||||
const [levelObject, setLevelObject] = useState<Object3D | null>(() => sceneRegistry.nodes.get(levelId) ?? null)
|
const [levelObject, setLevelObject] = useState<Object3D | null>(
|
||||||
|
() => sceneRegistry.nodes.get(levelId) ?? null,
|
||||||
|
)
|
||||||
|
|
||||||
const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
|
const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
|
||||||
|
|
||||||
@@ -110,11 +112,7 @@ const CeilingSelectionAffordance = ({
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
|
<group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
|
||||||
{corners.map((corner, index) => (
|
{corners.map((corner, index) => (
|
||||||
<CornerBracket
|
<CornerBracket ceiling={ceiling} corner={corner} key={`${ceiling.id}-corner-${index}`} />
|
||||||
ceiling={ceiling}
|
|
||||||
corner={corner}
|
|
||||||
key={`${ceiling.id}-corner-${index}`}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</group>,
|
</group>,
|
||||||
levelObject,
|
levelObject,
|
||||||
@@ -210,11 +208,7 @@ const BracketLeg = ({
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh onClick={onClick} position={position} rotation={[0, angle, 0]}>
|
||||||
onClick={onClick}
|
|
||||||
position={position}
|
|
||||||
rotation={[0, angle, 0]}
|
|
||||||
>
|
|
||||||
<boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
|
<boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
|
||||||
<meshBasicMaterial color={color} depthWrite={false} opacity={opacity} transparent />
|
<meshBasicMaterial color={color} depthWrite={false} opacity={opacity} transparent />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -234,7 +228,11 @@ function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketDat
|
|||||||
|
|
||||||
const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
|
const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
|
||||||
const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
|
const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
|
||||||
const cornerStrength = 1 - Math.abs(incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1])
|
const cornerStrength =
|
||||||
|
1 -
|
||||||
|
Math.abs(
|
||||||
|
incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
corner,
|
corner,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useEffect, useRef } from 'react'
|
|||||||
* Imperatively toggles the Three.js visibility of roof objects based on the
|
* Imperatively toggles the Three.js visibility of roof objects based on the
|
||||||
* editor selection — without causing React re-renders in RoofRenderer.
|
* editor selection — without causing React re-renders in RoofRenderer.
|
||||||
*
|
*
|
||||||
* When a roof-segment is selected:
|
* When a roof (or one of its segments) is selected:
|
||||||
* - merged-roof mesh is hidden
|
* - merged-roof mesh is hidden
|
||||||
* - segments-wrapper group is shown (individual segments visible for editing)
|
* - segments-wrapper group is shown (individual segments visible for editing)
|
||||||
* - all children are marked dirty so RoofSystem rebuilds their geometry
|
* - all children are marked dirty so RoofSystem rebuilds their geometry
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const StairEditSystem = () => {
|
|||||||
const segmentsWrapper = group.getObjectByName('segments-wrapper')
|
const segmentsWrapper = group.getObjectByName('segments-wrapper')
|
||||||
const isActive = activeStairIds.has(stairId)
|
const isActive = activeStairIds.has(stairId)
|
||||||
|
|
||||||
if (mergedMesh) mergedMesh.visible = !isActive && !isCurved
|
if (mergedMesh) mergedMesh.visible = !(isActive || isCurved)
|
||||||
if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved
|
if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved
|
||||||
|
|
||||||
if (stairNode?.children?.length) {
|
if (stairNode?.children?.length) {
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
@@ -1,13 +1,19 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNodeId, emitter, type GridEvent, useScene, type CeilingNode } from '@pascal-app/core'
|
import {
|
||||||
|
type AnyNodeId,
|
||||||
|
type CeilingNode,
|
||||||
|
emitter,
|
||||||
|
type GridEvent,
|
||||||
|
useScene,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { BufferGeometry, DoubleSide, Path, Shape, ShapeGeometry, Vector3 } from 'three'
|
||||||
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
import { BufferGeometry, DoubleSide, Path, Shape, ShapeGeometry, Vector3 } from 'three'
|
|
||||||
|
|
||||||
function snap(value: number) {
|
function snap(value: number) {
|
||||||
return Math.round(value * 2) / 2
|
return Math.round(value * 2) / 2
|
||||||
@@ -202,6 +208,7 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
|
|||||||
transparent
|
transparent
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
{/* @ts-ignore */}
|
||||||
<line geometry={previewOutlineGeometry} position={[0, (node.height ?? 2.5) + 0.02, 0]}>
|
<line geometry={previewOutlineGeometry} position={[0, (node.height ?? 2.5) + 0.02, 0]}>
|
||||||
<lineBasicMaterial color="#ffffff" depthWrite={false} opacity={0.95} transparent />
|
<lineBasicMaterial color="#ffffff" depthWrite={false} opacity={0.95} transparent />
|
||||||
</line>
|
</line>
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ export const CurveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|||||||
? event.localPosition[2]
|
? event.localPosition[2]
|
||||||
: snapScalarToGrid(event.localPosition[2], snapStep)
|
: snapScalarToGrid(event.localPosition[2], snapStep)
|
||||||
|
|
||||||
const offsetFromMidpoint =
|
const offsetFromMidpoint = -(
|
||||||
-(
|
|
||||||
(localX - chord.midpoint.x) * chord.normal.x +
|
(localX - chord.midpoint.x) * chord.normal.x +
|
||||||
(localZ - chord.midpoint.y) * chord.normal.y
|
(localZ - chord.midpoint.y) * chord.normal.y
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export const FenceTool: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<CursorSphere ref={cursorRef} height={FENCE_PREVIEW_HEIGHT} />
|
<CursorSphere height={FENCE_PREVIEW_HEIGHT} ref={cursorRef} />
|
||||||
<mesh layers={EDITOR_LAYER} ref={previewRef} renderOrder={1} visible={false}>
|
<mesh layers={EDITOR_LAYER} ref={previewRef} renderOrder={1} visible={false}>
|
||||||
<shapeGeometry />
|
<shapeGeometry />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
@@ -338,7 +338,7 @@ function DraftMeasurementLabel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
||||||
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px] font-semibold text-foreground shadow-lg backdrop-blur-md">
|
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -131,10 +131,12 @@ function getLinkedFenceSnapshots(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!samePoint(node.start, originalStart) &&
|
!(
|
||||||
!samePoint(node.start, originalEnd) &&
|
samePoint(node.start, originalStart) ||
|
||||||
!samePoint(node.end, originalStart) &&
|
samePoint(node.start, originalEnd) ||
|
||||||
!samePoint(node.end, originalEnd)
|
samePoint(node.end, originalStart) ||
|
||||||
|
samePoint(node.end, originalEnd)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -303,8 +305,9 @@ export const MoveFenceEndpointTool: React.FC<{ target: MovingFenceEndpoint }> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
const hasChanged =
|
const hasChanged = !(
|
||||||
!samePoint(preview.start, originalStart) || !samePoint(preview.end, originalEnd)
|
samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd)
|
||||||
|
)
|
||||||
|
|
||||||
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
@@ -406,7 +409,7 @@ export const MoveFenceEndpointTool: React.FC<{ target: MovingFenceEndpoint }> =
|
|||||||
>
|
>
|
||||||
<div className="translate-y-10">
|
<div className="translate-y-10">
|
||||||
<div
|
<div
|
||||||
className={`whitespace-nowrap rounded-full border px-2 py-1 text-[11px] font-medium shadow-lg backdrop-blur-md transition-colors ${
|
className={`whitespace-nowrap rounded-full border px-2 py-1 font-medium text-[11px] shadow-lg backdrop-blur-md transition-colors ${
|
||||||
altPressed
|
altPressed
|
||||||
? 'border-amber-500/70 bg-amber-500/15 text-amber-100'
|
? 'border-amber-500/70 bg-amber-500/15 text-amber-100'
|
||||||
: 'border-border/70 bg-background/90 text-foreground/80'
|
: 'border-border/70 bg-background/90 text-foreground/80'
|
||||||
@@ -430,7 +433,7 @@ function EndpointAngleLabel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
||||||
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px] font-semibold text-foreground shadow-lg backdrop-blur-md">
|
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import type * as THREE from 'three'
|
|||||||
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { snapFenceDraftPoint } from './fence-drafting'
|
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
import { snapFenceDraftPoint } from './fence-drafting'
|
||||||
|
|
||||||
function samePoint(a: [number, number], b: [number, number]) {
|
function samePoint(a: [number, number], b: [number, number]) {
|
||||||
return a[0] === b[0] && a[1] === b[1]
|
return a[0] === b[0] && a[1] === b[1]
|
||||||
@@ -50,10 +50,12 @@ function getLinkedFenceSnapshots(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!samePoint(node.start, originalStart) &&
|
!(
|
||||||
!samePoint(node.start, originalEnd) &&
|
samePoint(node.start, originalStart) ||
|
||||||
!samePoint(node.end, originalStart) &&
|
samePoint(node.start, originalEnd) ||
|
||||||
!samePoint(node.end, originalEnd)
|
samePoint(node.end, originalStart) ||
|
||||||
|
samePoint(node.end, originalEnd)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -164,7 +166,9 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyNodePreview = (updates: Array<{ id: FenceNode['id']; start: [number, number]; end: [number, number] }>) => {
|
const applyNodePreview = (
|
||||||
|
updates: Array<{ id: FenceNode['id']; start: [number, number]; end: [number, number] }>,
|
||||||
|
) => {
|
||||||
useScene.getState().updateNodes(
|
useScene.getState().updateNodes(
|
||||||
updates.map((entry) => ({
|
updates.map((entry) => ({
|
||||||
id: entry.id as AnyNodeId,
|
id: entry.id as AnyNodeId,
|
||||||
@@ -275,13 +279,13 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|||||||
emitter.on('tool:cancel', onCancel)
|
emitter.on('tool:cancel', onCancel)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!wasCommitted) {
|
if (wasCommitted) {
|
||||||
clearPreviewState()
|
|
||||||
} else {
|
|
||||||
useLiveTransforms.getState().clear(nodeId)
|
useLiveTransforms.getState().clear(nodeId)
|
||||||
for (const linkedFence of linkedOriginalsRef.current) {
|
for (const linkedFence of linkedOriginalsRef.current) {
|
||||||
useLiveTransforms.getState().clear(linkedFence.id)
|
useLiveTransforms.getState().clear(linkedFence.id)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
clearPreviewState()
|
||||||
}
|
}
|
||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
emitter.off('grid:move', onGridMove)
|
emitter.off('grid:move', onGridMove)
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
|
|||||||
return <>{cursor}</>
|
return <>{cursor}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MoveTool: React.FC<{
|
export const MoveTool: React.FC = () => {
|
||||||
onSpawnMoved?: (nodeId: SpawnNode['id']) => void
|
|
||||||
}> = ({ onSpawnMoved }) => {
|
|
||||||
const movingNode = useEditor((state) => state.movingNode)
|
const movingNode = useEditor((state) => state.movingNode)
|
||||||
|
|
||||||
if (!movingNode) return null
|
if (!movingNode) return null
|
||||||
@@ -100,15 +98,14 @@ export const MoveTool: React.FC<{
|
|||||||
return <MoveBuildingContent node={movingNode as BuildingNode} />
|
return <MoveBuildingContent node={movingNode as BuildingNode} />
|
||||||
if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
|
if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
|
||||||
if (movingNode.type === 'window') return <MoveWindowTool node={movingNode as WindowNode} />
|
if (movingNode.type === 'window') return <MoveWindowTool node={movingNode as WindowNode} />
|
||||||
if (movingNode.type === 'fence') return <MoveFenceTool node={movingNode as FenceNode} />
|
|
||||||
if (movingNode.type === 'ceiling') return <MoveCeilingTool node={movingNode as CeilingNode} />
|
if (movingNode.type === 'ceiling') return <MoveCeilingTool node={movingNode as CeilingNode} />
|
||||||
if (movingNode.type === 'column') return <MoveColumnTool node={movingNode as ColumnNode} />
|
if (movingNode.type === 'column') return <MoveColumnTool node={movingNode as ColumnNode} />
|
||||||
if (movingNode.type === 'slab') return <MoveSlabTool node={movingNode as SlabNode} />
|
if (movingNode.type === 'slab') return <MoveSlabTool node={movingNode as SlabNode} />
|
||||||
if (movingNode.type === 'wall') return <MoveWallTool node={movingNode as WallNode} />
|
if (movingNode.type === 'wall') return <MoveWallTool node={movingNode as WallNode} />
|
||||||
|
if (movingNode.type === 'fence') return <MoveFenceTool node={movingNode as FenceNode} />
|
||||||
if (movingNode.type === 'roof' || movingNode.type === 'roof-segment')
|
if (movingNode.type === 'roof' || movingNode.type === 'roof-segment')
|
||||||
return <MoveRoofTool node={movingNode as RoofNode | RoofSegmentNode} />
|
return <MoveRoofTool node={movingNode as RoofNode | RoofSegmentNode} />
|
||||||
if (movingNode.type === 'spawn')
|
if (movingNode.type === 'spawn') return <MoveSpawnTool node={movingNode as SpawnNode} />
|
||||||
return <MoveSpawnTool node={movingNode as SpawnNode} onCommitted={onSpawnMoved} />
|
|
||||||
if (movingNode.type === 'stair' || movingNode.type === 'stair-segment')
|
if (movingNode.type === 'stair' || movingNode.type === 'stair-segment')
|
||||||
return <MoveRoofTool node={movingNode as StairNode | StairSegmentNode} />
|
return <MoveRoofTool node={movingNode as StairNode | StairSegmentNode} />
|
||||||
return <MoveItemContent movingNode={movingNode as ItemNode} />
|
return <MoveItemContent movingNode={movingNode as ItemNode} />
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ function positiveModulo(value: number, divisor: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snaps a position to 0.5 grid, with an offset to align item edges to grid lines.
|
* Snaps a position to the active grid step, aligning item edges to grid lines.
|
||||||
* For items with dimensions like 2.5, the center would be at 1.25 from the edge,
|
|
||||||
* which doesn't align with 0.5 grid. This adds an offset so edges align instead.
|
|
||||||
*/
|
*/
|
||||||
export function snapToGrid(position: number, dimension: number, step = getGridSnapStep()): number {
|
export function snapToGrid(position: number, dimension: number, step = getGridSnapStep()): number {
|
||||||
const halfDim = dimension / 2
|
const halfDim = dimension / 2
|
||||||
@@ -21,7 +19,7 @@ export function snapToGrid(position: number, dimension: number, step = getGridSn
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snap a value to 0.5 increments (used for wall-local positions).
|
* Snap a value to the active grid step (used for wall-local positions).
|
||||||
*/
|
*/
|
||||||
export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
||||||
return Math.round(value / step) * step
|
return Math.round(value / step) * step
|
||||||
|
|||||||
@@ -506,21 +506,22 @@ export const itemSurfaceStrategy = {
|
|||||||
|
|
||||||
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
||||||
|
|
||||||
|
// Counter-rotate so the draft's world Y rotation stays continuous when
|
||||||
|
// the user drags onto a rotated surface item. The cursor wireframe
|
||||||
|
// already shows the user's intended world rotation; we just need to
|
||||||
|
// store the right local value relative to the new parent.
|
||||||
|
const surfaceQuat = new Quaternion()
|
||||||
|
surfaceMesh.getWorldQuaternion(surfaceQuat)
|
||||||
|
const surfaceWorldY = new Euler().setFromQuaternion(surfaceQuat, 'YXZ').y
|
||||||
|
const localRotationY = ctx.currentCursorRotationY - surfaceWorldY
|
||||||
|
const draftRotation = ctx.draftItem?.rotation ?? [0, 0, 0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stateUpdate: { surface: 'item-surface', surfaceItemId: surfaceItem.id },
|
stateUpdate: { surface: 'item-surface', surfaceItemId: surfaceItem.id },
|
||||||
nodeUpdate: {
|
nodeUpdate: {
|
||||||
position: [x, y, z],
|
position: [x, y, z],
|
||||||
parentId: surfaceItem.id,
|
parentId: surfaceItem.id,
|
||||||
rotation: [
|
rotation: [draftRotation[0], localRotationY, draftRotation[2]],
|
||||||
(ctx.draftItem?.rotation ?? [0, 0, 0])[0],
|
|
||||||
(() => {
|
|
||||||
const surfaceQuat = new Quaternion()
|
|
||||||
surfaceMesh.getWorldQuaternion(surfaceQuat)
|
|
||||||
const surfaceWorldY = new Euler().setFromQuaternion(surfaceQuat, 'YXZ').y
|
|
||||||
return ctx.currentCursorRotationY - surfaceWorldY
|
|
||||||
})(),
|
|
||||||
(ctx.draftItem?.rotation ?? [0, 0, 0])[2],
|
|
||||||
] as [number, number, number],
|
|
||||||
},
|
},
|
||||||
cursorRotationY: ctx.currentCursorRotationY,
|
cursorRotationY: ctx.currentCursorRotationY,
|
||||||
gridPosition: [x, y, z],
|
gridPosition: [x, y, z],
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
useScene.getState().updateNode(draft.id, {
|
useScene.getState().updateNode(draft.id, {
|
||||||
position: updateProps.position ?? draft.position,
|
position: updateProps.position ?? draft.position,
|
||||||
rotation: updateProps.rotation ?? draft.rotation,
|
rotation: updateProps.rotation ?? draft.rotation,
|
||||||
scale: updateProps.scale ?? draft.scale,
|
|
||||||
side: updateProps.side ?? draft.side,
|
side: updateProps.side ?? draft.side,
|
||||||
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
||||||
parentId: parentId as string,
|
parentId: parentId as string,
|
||||||
|
|||||||
@@ -20,15 +20,12 @@ import { Html } from '@react-three/drei'
|
|||||||
import { useFrame } from '@react-three/fiber'
|
import { useFrame } from '@react-three/fiber'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Box3,
|
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
Euler,
|
Euler,
|
||||||
Float32BufferAttribute,
|
Float32BufferAttribute,
|
||||||
type Group,
|
type Group,
|
||||||
type LineSegments,
|
type LineSegments,
|
||||||
Matrix4,
|
|
||||||
type Mesh,
|
type Mesh,
|
||||||
type Object3D,
|
|
||||||
PlaneGeometry,
|
PlaneGeometry,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
@@ -117,79 +114,6 @@ function expandBoundsToGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreviewBoundsFromObject(object: Object3D | null): PreviewBounds | null {
|
|
||||||
if (!object) return null
|
|
||||||
|
|
||||||
object.updateWorldMatrix(true, true)
|
|
||||||
|
|
||||||
const inverseRootMatrix = new Matrix4().copy(object.matrixWorld).invert()
|
|
||||||
const localMatrix = new Matrix4()
|
|
||||||
const localBounds = new Box3()
|
|
||||||
const scratchBounds = new Box3()
|
|
||||||
const hasBounds = { current: false }
|
|
||||||
const registeredNodeObjects = new Set(sceneRegistry.nodes.values())
|
|
||||||
|
|
||||||
const expandBounds = (child: Object3D) => {
|
|
||||||
if (child !== object && registeredNodeObjects.has(child)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const mesh = child as Object3D & {
|
|
||||||
isMesh?: boolean
|
|
||||||
name?: string
|
|
||||||
geometry?: {
|
|
||||||
boundingBox: Box3 | null
|
|
||||||
computeBoundingBox?: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mesh.isMesh && mesh.name !== 'cutout' && mesh.geometry) {
|
|
||||||
if (!mesh.geometry.boundingBox && mesh.geometry.computeBoundingBox) {
|
|
||||||
mesh.geometry.computeBoundingBox()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mesh.geometry.boundingBox) {
|
|
||||||
localMatrix.copy(inverseRootMatrix).multiply(mesh.matrixWorld)
|
|
||||||
scratchBounds.copy(mesh.geometry.boundingBox).applyMatrix4(localMatrix)
|
|
||||||
if (Number.isFinite(scratchBounds.min.x) && Number.isFinite(scratchBounds.max.x)) {
|
|
||||||
if (!hasBounds.current) {
|
|
||||||
localBounds.copy(scratchBounds)
|
|
||||||
hasBounds.current = true
|
|
||||||
} else {
|
|
||||||
localBounds.union(scratchBounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const grandchild of child.children) {
|
|
||||||
expandBounds(grandchild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const child of object.children) {
|
|
||||||
expandBounds(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasBounds.current) return null
|
|
||||||
|
|
||||||
const size = new Vector3()
|
|
||||||
const center = new Vector3()
|
|
||||||
localBounds.getSize(size)
|
|
||||||
localBounds.getCenter(center)
|
|
||||||
|
|
||||||
if (size.x <= 0 || size.y <= 0 || size.z <= 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
min: [localBounds.min.x, localBounds.min.y, localBounds.min.z],
|
|
||||||
max: [localBounds.max.x, localBounds.max.y, localBounds.max.z],
|
|
||||||
dimensions: [size.x, size.y, size.z],
|
|
||||||
center: [center.x, center.y, center.z],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFallbackPreviewBounds(
|
function getFallbackPreviewBounds(
|
||||||
item: import('@pascal-app/core').ItemNode | null,
|
item: import('@pascal-app/core').ItemNode | null,
|
||||||
asset: AssetInput,
|
asset: AssetInput,
|
||||||
@@ -366,7 +290,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
)
|
)
|
||||||
const shiftFreeRef = useRef(false)
|
const shiftFreeRef = useRef(false)
|
||||||
const previewBoundsSignatureRef = useRef<string | null>(null)
|
const previewBoundsSignatureRef = useRef<string | null>(null)
|
||||||
const meshPreviewAppliedRef = useRef(false)
|
|
||||||
const [dimensionBounds, setDimensionBounds] = useState<PreviewBounds | null>(null)
|
const [dimensionBounds, setDimensionBounds] = useState<PreviewBounds | null>(null)
|
||||||
|
|
||||||
// Store config callbacks in refs to avoid re-running effect when they change
|
// Store config callbacks in refs to avoid re-running effect when they change
|
||||||
@@ -503,7 +426,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!asset) return
|
if (!asset) return
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
meshPreviewAppliedRef.current = false
|
|
||||||
|
|
||||||
const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
||||||
|
|
||||||
@@ -1264,21 +1186,17 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
window.addEventListener('contextmenu', onContextMenu)
|
window.addEventListener('contextmenu', onContextMenu)
|
||||||
|
|
||||||
// ---- Bounding box geometry ----
|
// ---- Bounding box geometry ----
|
||||||
|
// Always derive the wireframe from `asset.dimensions × scale` rather than
|
||||||
|
// the rendered mesh bounds. Asset dimensions describe the item's footprint
|
||||||
|
// (e.g. only the trunk for a palm tree), while the mesh bbox would include
|
||||||
|
// foliage or other visual overhang the snap logic intentionally ignores.
|
||||||
|
|
||||||
const draft = draftNode.current
|
const draft = draftNode.current
|
||||||
const fallbackBounds = expandBoundsToGrid(
|
const previewBounds = expandBoundsToGrid(
|
||||||
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
||||||
asset.attachTo,
|
asset.attachTo,
|
||||||
gridSnapStep,
|
gridSnapStep,
|
||||||
)
|
)
|
||||||
const previewBounds = draft
|
|
||||||
? expandBoundsToGrid(
|
|
||||||
getPreviewBoundsFromObject(sceneRegistry.nodes.get(draft.id) ?? null) ??
|
|
||||||
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
|
||||||
asset.attachTo,
|
|
||||||
gridSnapStep,
|
|
||||||
)
|
|
||||||
: fallbackBounds
|
|
||||||
updatePreviewGeometry(previewBounds)
|
updatePreviewGeometry(previewBounds)
|
||||||
updateDimensionGuides(previewBounds)
|
updateDimensionGuides(previewBounds)
|
||||||
|
|
||||||
@@ -1324,7 +1242,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
tearingDown = true
|
tearingDown = true
|
||||||
meshPreviewAppliedRef.current = false
|
|
||||||
unsubDraftWatch()
|
unsubDraftWatch()
|
||||||
// Clear live transform for any remaining draft
|
// Clear live transform for any remaining draft
|
||||||
if (draftNode.current) {
|
if (draftNode.current) {
|
||||||
@@ -1358,17 +1275,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!asset) return
|
if (!asset) return
|
||||||
const draft = draftNode.current
|
const draft = draftNode.current
|
||||||
const fallbackBounds = expandBoundsToGrid(
|
const previewBounds = expandBoundsToGrid(
|
||||||
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
getFallbackPreviewBounds(draft, asset, asset.attachTo),
|
||||||
asset.attachTo,
|
asset.attachTo,
|
||||||
gridSnapStep,
|
gridSnapStep,
|
||||||
)
|
)
|
||||||
const meshBounds = draft
|
|
||||||
? getPreviewBoundsFromObject(sceneRegistry.nodes.get(draft.id) ?? null)
|
|
||||||
: null
|
|
||||||
const previewBounds = meshBounds
|
|
||||||
? expandBoundsToGrid(meshBounds, asset.attachTo, gridSnapStep)
|
|
||||||
: fallbackBounds
|
|
||||||
updatePreviewGeometry(previewBounds)
|
updatePreviewGeometry(previewBounds)
|
||||||
updateDimensionGuides(previewBounds)
|
updateDimensionGuides(previewBounds)
|
||||||
}, [gridSnapStep, asset, draftNode])
|
}, [gridSnapStep, asset, draftNode])
|
||||||
@@ -1388,19 +1299,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
if (!draftNode.current) return
|
if (!draftNode.current) return
|
||||||
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
||||||
if (!mesh) return
|
if (!mesh) return
|
||||||
if (!meshPreviewAppliedRef.current) {
|
|
||||||
const previewBounds = getPreviewBoundsFromObject(mesh)
|
|
||||||
if (previewBounds) {
|
|
||||||
const expandedBounds = expandBoundsToGrid(
|
|
||||||
previewBounds,
|
|
||||||
asset.attachTo,
|
|
||||||
useEditor.getState().gridSnapStep,
|
|
||||||
)
|
|
||||||
updatePreviewGeometry(expandedBounds)
|
|
||||||
updateDimensionGuides(expandedBounds)
|
|
||||||
meshPreviewAppliedRef.current = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide wall/ceiling-attached items when between surfaces (only cursor visible)
|
// Hide wall/ceiling-attached items when between surfaces (only cursor visible)
|
||||||
if (asset.attachTo && placementState.current.surface === 'floor') {
|
if (asset.attachTo && placementState.current.surface === 'floor') {
|
||||||
@@ -1490,22 +1388,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
const measurementContent = (
|
const measurementContent = (
|
||||||
<>
|
<>
|
||||||
<lineSegments
|
<lineSegments
|
||||||
layers={EDITOR_LAYER}
|
|
||||||
geometry={initialWidthGuideGeometry}
|
geometry={initialWidthGuideGeometry}
|
||||||
|
layers={EDITOR_LAYER}
|
||||||
material={measurementMaterial}
|
material={measurementMaterial}
|
||||||
ref={measurementWidthRef}
|
ref={measurementWidthRef}
|
||||||
renderOrder={998}
|
renderOrder={998}
|
||||||
/>
|
/>
|
||||||
<lineSegments
|
<lineSegments
|
||||||
layers={EDITOR_LAYER}
|
|
||||||
geometry={initialDepthGuideGeometry}
|
geometry={initialDepthGuideGeometry}
|
||||||
|
layers={EDITOR_LAYER}
|
||||||
material={measurementMaterial}
|
material={measurementMaterial}
|
||||||
ref={measurementDepthRef}
|
ref={measurementDepthRef}
|
||||||
renderOrder={998}
|
renderOrder={998}
|
||||||
/>
|
/>
|
||||||
<lineSegments
|
<lineSegments
|
||||||
layers={EDITOR_LAYER}
|
|
||||||
geometry={initialHeightGuideGeometry}
|
geometry={initialHeightGuideGeometry}
|
||||||
|
layers={EDITOR_LAYER}
|
||||||
material={measurementMaterial}
|
material={measurementMaterial}
|
||||||
ref={measurementHeightRef}
|
ref={measurementHeightRef}
|
||||||
renderOrder={998}
|
renderOrder={998}
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ export const MoveRoofTool: React.FC<{
|
|||||||
|
|
||||||
const localToWorldPoint = (localPoint: WallPlanPoint, y: number): [number, number, number] => {
|
const localToWorldPoint = (localPoint: WallPlanPoint, y: number): [number, number, number] => {
|
||||||
if (buildingObj) {
|
if (buildingObj) {
|
||||||
const worldPoint = buildingObj.localToWorld(new THREE.Vector3(localPoint[0], y, localPoint[1]))
|
const worldPoint = buildingObj.localToWorld(
|
||||||
|
new THREE.Vector3(localPoint[0], y, localPoint[1]),
|
||||||
|
)
|
||||||
return [worldPoint.x, worldPoint.y, worldPoint.z]
|
return [worldPoint.x, worldPoint.y, worldPoint.z]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +213,10 @@ export const MoveRoofTool: React.FC<{
|
|||||||
})
|
})
|
||||||
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
||||||
|
|
||||||
if (previousGridPosRef.current && (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) {
|
if (
|
||||||
|
previousGridPosRef.current &&
|
||||||
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
||||||
|
) {
|
||||||
sfxEmitter.emit('sfx:grid-snap')
|
sfxEmitter.emit('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,17 @@ function collectNodeIdsInBounds(bounds: Bounds): string[] {
|
|||||||
|
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
|
|
||||||
if (phase === 'structure' && structureLayer === 'elements') {
|
if (phase === 'structure' && structureLayer === 'zones') {
|
||||||
|
for (const childId of levelNode.children) {
|
||||||
|
const node = nodes[childId as AnyNodeId]
|
||||||
|
if (!node || node.type !== 'zone') continue
|
||||||
|
const zone = node as ZoneNode
|
||||||
|
if (polygonIntersectsBounds(zone.polygon, bounds)) {
|
||||||
|
result.push(zone.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// structure (elements) and furnish: collect all node types
|
||||||
for (const childId of levelNode.children) {
|
for (const childId of levelNode.children) {
|
||||||
const node = nodes[childId as AnyNodeId]
|
const node = nodes[childId as AnyNodeId]
|
||||||
if (!node) continue
|
if (!node) continue
|
||||||
@@ -240,22 +250,7 @@ function collectNodeIdsInBounds(bounds: Bounds): string[] {
|
|||||||
if (objectBoundsIntersectsBounds(node.id, bounds)) {
|
if (objectBoundsIntersectsBounds(node.id, bounds)) {
|
||||||
result.push(node.id)
|
result.push(node.id)
|
||||||
}
|
}
|
||||||
}
|
} else if (node.type === 'item') {
|
||||||
}
|
|
||||||
} else if (phase === 'structure' && structureLayer === 'zones') {
|
|
||||||
for (const childId of levelNode.children) {
|
|
||||||
const node = nodes[childId as AnyNodeId]
|
|
||||||
if (!node || node.type !== 'zone') continue
|
|
||||||
const zone = node as ZoneNode
|
|
||||||
if (polygonIntersectsBounds(zone.polygon, bounds)) {
|
|
||||||
result.push(zone.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (phase === 'furnish') {
|
|
||||||
for (const childId of levelNode.children) {
|
|
||||||
const node = nodes[childId as AnyNodeId]
|
|
||||||
if (!node) continue
|
|
||||||
if (node.type === 'item') {
|
|
||||||
const item = node as ItemNode
|
const item = node as ItemNode
|
||||||
if (item.asset.category === 'door' || item.asset.category === 'window') continue
|
if (item.asset.category === 'door' || item.asset.category === 'window') continue
|
||||||
const xz = getNodeWorldXZ(item.id)
|
const xz = getNodeWorldXZ(item.id)
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function getSegmentAngleReferenceAtPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projected = getProjectedPointOnSegment(point, segment)
|
const projected = getProjectedPointOnSegment(point, segment)
|
||||||
if (!projected || !pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE)) {
|
if (!(projected && pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE))) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
|||||||
door: DoorTool,
|
door: DoorTool,
|
||||||
item: ItemTool,
|
item: ItemTool,
|
||||||
zone: ZoneTool,
|
zone: ZoneTool,
|
||||||
|
spawn: SpawnTool,
|
||||||
window: WindowTool,
|
window: WindowTool,
|
||||||
},
|
},
|
||||||
furnish: {
|
furnish: {
|
||||||
@@ -63,10 +64,9 @@ export const ToolManager: React.FC = () => {
|
|||||||
const curvingFence = useEditor((state) => state.curvingFence)
|
const curvingFence = useEditor((state) => state.curvingFence)
|
||||||
const editingHole = useEditor((state) => state.editingHole)
|
const editingHole = useEditor((state) => state.editingHole)
|
||||||
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
||||||
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
||||||
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
||||||
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
||||||
const setSelection = useViewer((state) => state.setSelection)
|
const buildingId = useViewer((state) => state.selection.buildingId)
|
||||||
|
const activeLevelId = useViewer((state) => state.selection.levelId)
|
||||||
const nodes = useScene((state) => state.nodes)
|
const nodes = useScene((state) => state.nodes)
|
||||||
|
|
||||||
// Building transform for the local group — all building-relative tools live inside this group
|
// Building transform for the local group — all building-relative tools live inside this group
|
||||||
@@ -128,13 +128,13 @@ export const ToolManager: React.FC = () => {
|
|||||||
|
|
||||||
const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null
|
const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null
|
||||||
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
||||||
setSelection({ selectedIds: [nodeId] })
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
|
||||||
{/* World-space tools: site boundary and building movement operate in world coordinates */}
|
{/* World-space tools: site boundary and building movement operate in world coordinates */}
|
||||||
|
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
||||||
{movingNode?.type === 'building' && <MoveTool onSpawnMoved={handlePlacedNodeSelected} />}
|
{movingNode?.type === 'building' && <MoveTool onSpawnMoved={handlePlacedNodeSelected} />}
|
||||||
|
|
||||||
{/* Building-local group: all other tools are relative to the selected building.
|
{/* Building-local group: all other tools are relative to the selected building.
|
||||||
@@ -162,13 +162,13 @@ export const ToolManager: React.FC = () => {
|
|||||||
{movingNode && movingNode.type !== 'building' && (
|
{movingNode && movingNode.type !== 'building' && (
|
||||||
<MoveTool onSpawnMoved={handlePlacedNodeSelected} />
|
<MoveTool onSpawnMoved={handlePlacedNodeSelected} />
|
||||||
)}
|
)}
|
||||||
{!movingNode && showBuildTool && tool === 'spawn' && (
|
{!movingNode && BuildToolComponent && tool === 'spawn' ? (
|
||||||
<SpawnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
|
<SpawnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
|
||||||
)}
|
) : !movingNode && showBuildTool && tool === 'column' ? (
|
||||||
{!movingNode && showBuildTool && tool === 'column' && (
|
<ColumnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
|
||||||
<ColumnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
|
) : !movingNode && BuildToolComponent && tool !== 'column' ? (
|
||||||
)}
|
<BuildToolComponent />
|
||||||
{!movingNode && BuildToolComponent && tool !== 'column' && <BuildToolComponent />}
|
) : null}
|
||||||
</group>
|
</group>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -81,15 +81,17 @@ export const CurveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
? event.localPosition[2]
|
? event.localPosition[2]
|
||||||
: snapScalarToGrid(event.localPosition[2], snapStep)
|
: snapScalarToGrid(event.localPosition[2], snapStep)
|
||||||
|
|
||||||
const offsetFromMidpoint =
|
const offsetFromMidpoint = -(
|
||||||
-(
|
|
||||||
(localX - chord.midpoint.x) * chord.normal.x +
|
(localX - chord.midpoint.x) * chord.normal.x +
|
||||||
(localZ - chord.midpoint.y) * chord.normal.y
|
(localZ - chord.midpoint.y) * chord.normal.y
|
||||||
)
|
)
|
||||||
const snappedOffset = shiftPressedRef.current
|
const snappedOffset = shiftPressedRef.current
|
||||||
? offsetFromMidpoint
|
? offsetFromMidpoint
|
||||||
: snapScalarToGrid(offsetFromMidpoint, snapStep)
|
: snapScalarToGrid(offsetFromMidpoint, snapStep)
|
||||||
const nextCurveOffset = normalizeWallCurveOffset(node, Math.max(-maxCurveOffset, Math.min(maxCurveOffset, snappedOffset)))
|
const nextCurveOffset = normalizeWallCurveOffset(
|
||||||
|
node,
|
||||||
|
Math.max(-maxCurveOffset, Math.min(maxCurveOffset, snappedOffset)),
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
previousCurveOffsetRef.current !== null &&
|
previousCurveOffsetRef.current !== null &&
|
||||||
|
|||||||
@@ -112,10 +112,12 @@ function getLinkedWallSnapshots(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!samePoint(node.start, originalStart) &&
|
!(
|
||||||
!samePoint(node.start, originalEnd) &&
|
samePoint(node.start, originalStart) ||
|
||||||
!samePoint(node.end, originalStart) &&
|
samePoint(node.start, originalEnd) ||
|
||||||
!samePoint(node.end, originalEnd)
|
samePoint(node.end, originalStart) ||
|
||||||
|
samePoint(node.end, originalEnd)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -286,8 +288,9 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
const hasChanged =
|
const hasChanged = !(
|
||||||
!samePoint(preview.start, originalStart) || !samePoint(preview.end, originalEnd)
|
samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd)
|
||||||
|
)
|
||||||
|
|
||||||
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
@@ -391,7 +394,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
>
|
>
|
||||||
<div className="translate-y-10">
|
<div className="translate-y-10">
|
||||||
<div
|
<div
|
||||||
className={`whitespace-nowrap rounded-full border px-2 py-1 text-[11px] font-medium shadow-lg backdrop-blur-md transition-colors ${
|
className={`whitespace-nowrap rounded-full border px-2 py-1 font-medium text-[11px] shadow-lg backdrop-blur-md transition-colors ${
|
||||||
altPressed
|
altPressed
|
||||||
? 'border-amber-500/80 bg-amber-500/15 text-amber-100'
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100'
|
||||||
: 'border-border bg-background/95 text-muted-foreground'
|
: 'border-border bg-background/95 text-muted-foreground'
|
||||||
@@ -415,7 +418,7 @@ function EndpointAngleLabel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
||||||
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px] font-semibold text-foreground shadow-lg backdrop-blur-md">
|
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ function getLinkedWallSnapshots(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!samePoint(node.start, originalStart) &&
|
!(
|
||||||
!samePoint(node.start, originalEnd) &&
|
samePoint(node.start, originalStart) ||
|
||||||
!samePoint(node.end, originalStart) &&
|
samePoint(node.start, originalEnd) ||
|
||||||
!samePoint(node.end, originalEnd)
|
samePoint(node.end, originalStart) ||
|
||||||
|
samePoint(node.end, originalEnd)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
+3
-3
@@ -156,6 +156,8 @@ export const WallTool: React.FC = () => {
|
|||||||
const unit = useViewer((state) => state.unit)
|
const unit = useViewer((state) => state.unit)
|
||||||
const cursorRef = useRef<Group>(null)
|
const cursorRef = useRef<Group>(null)
|
||||||
const wallPreviewRef = useRef<Mesh>(null!)
|
const wallPreviewRef = useRef<Mesh>(null!)
|
||||||
|
// All positions are building-local: this tool is inside the ToolManager building group,
|
||||||
|
// so local coords are used for both data and visual positioning.
|
||||||
const startingPoint = useRef(new Vector3(0, 0, 0))
|
const startingPoint = useRef(new Vector3(0, 0, 0))
|
||||||
const endingPoint = useRef(new Vector3(0, 0, 0))
|
const endingPoint = useRef(new Vector3(0, 0, 0))
|
||||||
const buildingState = useRef(0)
|
const buildingState = useRef(0)
|
||||||
@@ -166,8 +168,6 @@ export const WallTool: React.FC = () => {
|
|||||||
let gridPosition: WallPlanPoint = [0, 0]
|
let gridPosition: WallPlanPoint = [0, 0]
|
||||||
let previousWallEnd: [number, number] | null = null
|
let previousWallEnd: [number, number] | null = null
|
||||||
|
|
||||||
// All positions are building-local: this tool is inside the ToolManager building group,
|
|
||||||
// so local coords are used for both data and visual positioning.
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
if (!(cursorRef.current && wallPreviewRef.current)) return
|
if (!(cursorRef.current && wallPreviewRef.current)) return
|
||||||
|
|
||||||
@@ -324,7 +324,7 @@ function DraftMeasurementLabel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
||||||
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px] font-semibold text-foreground shadow-lg backdrop-blur-md">
|
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
Executable → Regular
+20
-5
@@ -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 { EDITOR_LAYER } from './../../../lib/constants'
|
import { EDITOR_LAYER } from './../../../lib/constants'
|
||||||
|
import { sfxEmitter } from './../../../lib/sfx-bus'
|
||||||
import useEditor from './../../../store/use-editor'
|
import useEditor from './../../../store/use-editor'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
|
||||||
@@ -67,6 +68,9 @@ const commitZoneDrawing = (levelId: LevelNode['id'], points: Array<[number, numb
|
|||||||
|
|
||||||
// Select the newly created zone
|
// Select the newly created zone
|
||||||
useViewer.getState().setSelection({ zoneId: zone.id })
|
useViewer.getState().setSelection({ zoneId: zone.id })
|
||||||
|
|
||||||
|
// Play structure build sound
|
||||||
|
sfxEmitter.emit('sfx:structure-build')
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreviewState = {
|
type PreviewState = {
|
||||||
@@ -86,6 +90,7 @@ export const ZoneTool: React.FC = () => {
|
|||||||
const mainLineRef = useRef<Line>(null!)
|
const mainLineRef = useRef<Line>(null!)
|
||||||
const closingLineRef = useRef<Line>(null!)
|
const closingLineRef = useRef<Line>(null!)
|
||||||
const pointsRef = useRef<Array<[number, number]>>([])
|
const pointsRef = useRef<Array<[number, number]>>([])
|
||||||
|
const previousSnappedPointRef = useRef<[number, number] | null>(null)
|
||||||
const levelYRef = useRef(0) // Track current level Y position
|
const levelYRef = useRef(0) // Track current level Y position
|
||||||
const currentLevelId = useViewer((state) => state.selection.levelId)
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
||||||
const setTool = useEditor((state) => state.setTool)
|
const setTool = useEditor((state) => state.setTool)
|
||||||
@@ -181,12 +186,22 @@ export const ZoneTool: React.FC = () => {
|
|||||||
|
|
||||||
// If we have points, snap to axis from last point
|
// If we have points, snap to axis from last point
|
||||||
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
||||||
if (lastPoint) {
|
const displayPoint = lastPoint
|
||||||
const snapped = calculateSnapPoint(lastPoint, cursorPosition)
|
? calculateSnapPoint(lastPoint, cursorPosition)
|
||||||
cursorRef.current.position.set(snapped[0], event.localPosition[1], snapped[1])
|
: cursorPosition
|
||||||
} else {
|
|
||||||
cursorRef.current.position.set(gridX, event.localPosition[1], gridZ)
|
// Play snap sound when the snapped position changes during drawing
|
||||||
|
if (
|
||||||
|
pointsRef.current.length > 0 &&
|
||||||
|
previousSnappedPointRef.current &&
|
||||||
|
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
||||||
|
displayPoint[1] !== previousSnappedPointRef.current[1])
|
||||||
|
) {
|
||||||
|
sfxEmitter.emit('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
|
previousSnappedPointRef.current = displayPoint
|
||||||
|
|
||||||
|
cursorRef.current.position.set(displayPoint[0], event.localPosition[1], displayPoint[1])
|
||||||
|
|
||||||
updatePreview()
|
updatePreview()
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
+7
-1
@@ -104,7 +104,9 @@ export function ControlModes() {
|
|||||||
const setPhase = useEditor((state) => state.setPhase)
|
const setPhase = useEditor((state) => state.setPhase)
|
||||||
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
||||||
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
||||||
const primeMaterialPaintFromSelection = useEditor((state) => state.primeMaterialPaintFromSelection)
|
const primeMaterialPaintFromSelection = useEditor(
|
||||||
|
(state) => state.primeMaterialPaintFromSelection,
|
||||||
|
)
|
||||||
const levelId = useViewer((s) => s.selection.levelId)
|
const levelId = useViewer((s) => s.selection.levelId)
|
||||||
|
|
||||||
// Only subscribe to the primitive `level` number — when walls are added to
|
// Only subscribe to the primitive `level` number — when walls are added to
|
||||||
@@ -148,6 +150,8 @@ export function ControlModes() {
|
|||||||
// setPhase('site') calls viewer.resetSelection() which clears levelId,
|
// setPhase('site') calls viewer.resetSelection() which clears levelId,
|
||||||
// breaking the 2D floorplan (it needs a level to render the SVG).
|
// breaking the 2D floorplan (it needs a level to render the SVG).
|
||||||
useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
|
useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
|
||||||
|
// Clear object selection so the polygon editor handles receive pointer events
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [] })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -188,6 +192,8 @@ export function ControlModes() {
|
|||||||
} else {
|
} else {
|
||||||
setPhase('furnish')
|
setPhase('furnish')
|
||||||
setMode('build')
|
setMode('build')
|
||||||
|
// Auto-switch sidebar to the items panel so the user can pick furniture
|
||||||
|
useEditor.getState().setActiveSidebarPanel('items')
|
||||||
}
|
}
|
||||||
} else if (id === 'zone') {
|
} else if (id === 'zone') {
|
||||||
if (getIsActive('zone')) {
|
if (getIsActive('zone')) {
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
'use client'
|
import type { CatalogCategory } from './../../../store/use-editor'
|
||||||
|
|
||||||
import NextImage from 'next/image'
|
|
||||||
import { cn } from './../../../lib/utils'
|
|
||||||
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
||||||
import { ActionButton } from './action-button'
|
|
||||||
|
|
||||||
export type FurnishToolConfig = {
|
export type FurnishToolConfig = {
|
||||||
id: 'item'
|
id: 'item'
|
||||||
@@ -12,91 +7,10 @@ export type FurnishToolConfig = {
|
|||||||
catalogCategory: CatalogCategory
|
catalogCategory: CatalogCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Furnish mode tools: furniture, appliances, decoration (painting is now a control mode)
|
|
||||||
export const furnishTools: FurnishToolConfig[] = [
|
export const furnishTools: FurnishToolConfig[] = [
|
||||||
{
|
{ id: 'item', iconSrc: '/icons/couch.png', label: 'Furniture', catalogCategory: 'furniture' },
|
||||||
id: 'item',
|
{ id: 'item', iconSrc: '/icons/appliance.png', label: 'Appliance', catalogCategory: 'appliance' },
|
||||||
iconSrc: '/icons/couch.png',
|
{ id: 'item', iconSrc: '/icons/kitchen.png', label: 'Kitchen', catalogCategory: 'kitchen' },
|
||||||
label: 'Furniture',
|
{ id: 'item', iconSrc: '/icons/bathroom.png', label: 'Bathroom', catalogCategory: 'bathroom' },
|
||||||
catalogCategory: 'furniture',
|
{ id: 'item', iconSrc: '/icons/tree.png', label: 'Outdoor', catalogCategory: 'outdoor' },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'item',
|
|
||||||
iconSrc: '/icons/appliance.png',
|
|
||||||
label: 'Appliance',
|
|
||||||
catalogCategory: 'appliance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'item',
|
|
||||||
iconSrc: '/icons/kitchen.png',
|
|
||||||
label: 'Kitchen',
|
|
||||||
catalogCategory: 'kitchen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'item',
|
|
||||||
iconSrc: '/icons/bathroom.png',
|
|
||||||
label: 'Bathroom',
|
|
||||||
catalogCategory: 'bathroom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'item',
|
|
||||||
iconSrc: '/icons/tree.png',
|
|
||||||
label: 'Outdoor',
|
|
||||||
catalogCategory: 'outdoor',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export function FurnishTools() {
|
|
||||||
const mode = useEditor((state) => state.mode)
|
|
||||||
const activeTool = useEditor((state) => state.tool)
|
|
||||||
const setActiveTool = useEditor((state) => state.setTool)
|
|
||||||
const setMode = useEditor((state) => state.setMode)
|
|
||||||
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
||||||
const setCatalogCategory = useEditor((state) => state.setCatalogCategory)
|
|
||||||
|
|
||||||
const hasActiveTool = furnishTools.some(
|
|
||||||
(tool) => mode === 'build' && activeTool === 'item' && catalogCategory === tool.catalogCategory,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1.5 px-1">
|
|
||||||
{furnishTools.map((tool, index) => {
|
|
||||||
// For item tools with catalog category, check both tool and category match
|
|
||||||
const isActive =
|
|
||||||
mode === 'build' && activeTool === 'item' && catalogCategory === tool.catalogCategory
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionButton
|
|
||||||
className={cn(
|
|
||||||
'rounded-lg duration-300',
|
|
||||||
isActive
|
|
||||||
? 'z-10 scale-110 bg-black/40 hover:bg-black/40'
|
|
||||||
: 'scale-95 bg-transparent opacity-60 grayscale hover:bg-black/20 hover:opacity-100 hover:grayscale-0',
|
|
||||||
)}
|
|
||||||
key={`${tool.id}-${tool.catalogCategory ?? index}`}
|
|
||||||
label={tool.label}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isActive) {
|
|
||||||
setCatalogCategory(tool.catalogCategory)
|
|
||||||
setActiveTool('item')
|
|
||||||
if (mode !== 'build') {
|
|
||||||
setMode('build')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<NextImage
|
|
||||||
alt={tool.label}
|
|
||||||
className="size-full object-contain"
|
|
||||||
height={28}
|
|
||||||
src={tool.iconSrc}
|
|
||||||
width={28}
|
|
||||||
/>
|
|
||||||
</ActionButton>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useScene } from '@pascal-app/core'
|
import { useScene } from '@pascal-app/core'
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { AnimatePresence, motion } from 'motion/react'
|
import { AnimatePresence, motion } from 'motion/react'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
|
||||||
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
|
||||||
import { useIsMobile } from './../../../hooks/use-mobile'
|
|
||||||
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
|
||||||
import { MaterialPicker } from './../../../components/ui/controls/material-picker'
|
import { MaterialPicker } from './../../../components/ui/controls/material-picker'
|
||||||
|
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
||||||
|
import { useIsMobile } from './../../../hooks/use-mobile'
|
||||||
|
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
||||||
import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
|
import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
|
||||||
import { cn } from './../../../lib/utils'
|
import { cn } from './../../../lib/utils'
|
||||||
import useEditor from './../../../store/use-editor'
|
import useEditor from './../../../store/use-editor'
|
||||||
import { ItemCatalog } from '../item-catalog/item-catalog'
|
|
||||||
import { CameraActions } from './camera-actions'
|
import { CameraActions } from './camera-actions'
|
||||||
import { ControlModes } from './control-modes'
|
import { ControlModes } from './control-modes'
|
||||||
import { FurnishTools } from './furnish-tools'
|
|
||||||
import { StructureTools } from './structure-tools'
|
import { StructureTools } from './structure-tools'
|
||||||
import { ViewToggles } from './view-toggles'
|
import { GridSnapControl, SecondaryToggles } from './view-toggles'
|
||||||
|
|
||||||
|
// Mobile bottom offset matches the viewer's overlap behind the sheet's
|
||||||
|
// rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits
|
||||||
|
// just above that strip instead of inside it.
|
||||||
|
const MOBILE_BOTTOM_OFFSET = 24
|
||||||
|
|
||||||
function PaintMaterialTray() {
|
function PaintMaterialTray() {
|
||||||
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
||||||
@@ -84,84 +87,16 @@ export function ActionMenu({ className }: { className?: string }) {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed bottom-6 left-1/2 z-50 -translate-x-1/2',
|
'left-1/2 z-50 -translate-x-1/2',
|
||||||
|
isMobile ? 'absolute origin-bottom scale-90' : 'fixed bottom-6',
|
||||||
'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
|
'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
|
||||||
'transition-colors duration-200 ease-out',
|
'transition-colors duration-200 ease-out',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
layout
|
layout
|
||||||
|
style={isMobile ? { bottom: MOBILE_BOTTOM_OFFSET } : undefined}
|
||||||
transition={transition}
|
transition={transition}
|
||||||
>
|
>
|
||||||
{/* Item Catalog Row - Only show when in build mode with item tool */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{mode === 'build' && tool === 'item' && catalogCategory && (
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
maxHeight: 160,
|
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 8,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
}}
|
|
||||||
className={cn('overflow-hidden border-border border-b px-2 py-2')}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
maxHeight: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
}}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
maxHeight: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
}}
|
|
||||||
transition={transition}
|
|
||||||
>
|
|
||||||
<ItemCatalog category={catalogCategory} key={catalogCategory} />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{phase === 'furnish' && mode === 'build' && (
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
maxHeight: 80,
|
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 8,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
'overflow-hidden border-border',
|
|
||||||
'max-h-20 border-b px-2 py-2 opacity-100',
|
|
||||||
)}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
maxHeight: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
}}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
maxHeight: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
}}
|
|
||||||
transition={transition}
|
|
||||||
>
|
|
||||||
<div className="mx-auto w-max">
|
|
||||||
<FurnishTools />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Structure Tools Row - Animated */}
|
{/* Structure Tools Row - Animated */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{phase === 'structure' && mode === 'build' && (
|
{phase === 'structure' && mode === 'build' && (
|
||||||
@@ -228,14 +163,28 @@ export function ActionMenu({ className }: { className?: string }) {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
{/* Control Mode Row - Always visible, centered */}
|
{isMobile ? (
|
||||||
|
<div className="flex flex-col items-stretch gap-0.5 px-2 py-1.5">
|
||||||
|
{/* Row 1: control modes only */}
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<ControlModes />
|
||||||
|
</div>
|
||||||
|
{/* Row 2: grid snap + secondary toggles (orbit + top view hidden) */}
|
||||||
|
<div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
|
||||||
|
<GridSnapControl />
|
||||||
|
<SecondaryToggles />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
||||||
<ControlModes />
|
<ControlModes />
|
||||||
<div className="mx-1 h-5 w-px bg-border" />
|
<div className="mx-1 h-5 w-px bg-border" />
|
||||||
<ViewToggles />
|
<GridSnapControl />
|
||||||
|
<SecondaryToggles />
|
||||||
<div className="mx-1 h-5 w-px bg-border" />
|
<div className="mx-1 h-5 w-px bg-border" />
|
||||||
<CameraActions />
|
<CameraActions />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
Executable → Regular
+19
-31
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react'
|
||||||
import {
|
import {
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
type BuildingNode,
|
type BuildingNode,
|
||||||
@@ -258,7 +259,7 @@ function GuidesControl() {
|
|||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
align="center"
|
align="center"
|
||||||
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
|
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
||||||
side="top"
|
side="top"
|
||||||
sideOffset={14}
|
sideOffset={14}
|
||||||
>
|
>
|
||||||
@@ -353,9 +354,9 @@ function GuidesControl() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Grid snap ──────────────────────────────────────────────────────────────
|
// ── Grid snap toggle ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function GridSnapControl() {
|
function GridSnapControl() {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
||||||
const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
|
const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
|
||||||
@@ -374,20 +375,7 @@ export function GridSnapControl() {
|
|||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon height={16} icon="lucide:grid-2x2" width={16} />
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3h7v7H3V3zm11 0h7v7h-7V3zm0 11h7v7h-7v-7zm-11 0h7v7H3v-7z"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="mt-1 font-medium text-[9px] leading-none">
|
<span className="mt-1 font-medium text-[9px] leading-none">
|
||||||
{formatGridSnapStep(gridSnapStep)}
|
{formatGridSnapStep(gridSnapStep)}
|
||||||
</span>
|
</span>
|
||||||
@@ -510,7 +498,7 @@ function ScansControl() {
|
|||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
align="center"
|
align="center"
|
||||||
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
|
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
||||||
side="top"
|
side="top"
|
||||||
sideOffset={14}
|
sideOffset={14}
|
||||||
>
|
>
|
||||||
@@ -605,6 +593,8 @@ function ScansControl() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Reference floor control ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function ReferenceFloorControl() {
|
function ReferenceFloorControl() {
|
||||||
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
||||||
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
||||||
@@ -694,7 +684,11 @@ function ReferenceFloorControl() {
|
|||||||
onClick={toggleReferenceFloor}
|
onClick={toggleReferenceFloor}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{showReferenceFloor ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
{showReferenceFloor ? (
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -728,9 +722,7 @@ function ReferenceFloorControl() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 flex-1 truncate">{levelName}</span>
|
<span className="min-w-0 flex-1 truncate">{levelName}</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">{index + 1} below</span>
|
||||||
{index + 1} below
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -757,24 +749,20 @@ function ReferenceFloorControl() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Main ViewToggles ────────────────────────────────────────────────────────
|
// ── Exports ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function ViewToggles() {
|
export { GridSnapControl }
|
||||||
|
|
||||||
|
export function SecondaryToggles() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{/* Scans (toggle + dropdown) */}
|
|
||||||
<ScansControl />
|
<ScansControl />
|
||||||
|
|
||||||
{/* Guides (toggle + dropdown) */}
|
|
||||||
<GuidesControl />
|
<GuidesControl />
|
||||||
|
|
||||||
<ReferenceFloorControl />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secondary toggles for mobile (grid snap + scans + guides)
|
export function ViewToggles() {
|
||||||
export function SecondaryToggles() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<GridSnapControl />
|
<GridSnapControl />
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ import {
|
|||||||
Video,
|
Video,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
||||||
import { runRedo, runUndo } from '../../../lib/history'
|
import { runRedo, runUndo } from '../../../lib/history'
|
||||||
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
||||||
import { useCommandRegistry } from '../../../store/use-command-registry'
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
||||||
import type { StructureTool } from '../../../store/use-editor'
|
import type { StructureTool } from '../../../store/use-editor'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
@@ -294,12 +294,14 @@ export function EditorCommands() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'editor.viewer.camera-snapshot',
|
id: 'editor.viewer.camera-snapshot',
|
||||||
label: 'Camera Snapshot',
|
label: 'Take Snapshot',
|
||||||
group: 'Viewer Controls',
|
group: 'Viewer Controls',
|
||||||
icon: <Camera className="h-4 w-4" />,
|
icon: <Camera className="h-4 w-4" />,
|
||||||
keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
|
keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
|
||||||
navigate: true,
|
execute: () => {
|
||||||
execute: () => navigateTo('camera-view'),
|
setOpen(false)
|
||||||
|
useEditor.getState().setCaptureMode(true)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── View ─────────────────────────────────────────────────────────────
|
// ── View ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Executable → Regular
+4
-255
@@ -27,15 +27,13 @@ interface CommandPaletteStore {
|
|||||||
setInputValue: (value: string) => void
|
setInputValue: (value: string) => void
|
||||||
navigateTo: (page: string) => void
|
navigateTo: (page: string) => void
|
||||||
goBack: () => void
|
goBack: () => void
|
||||||
cameraScope: { nodeId: string; label: string } | null
|
|
||||||
setCameraScope: (scope: { nodeId: string; label: string } | null) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
||||||
open: false,
|
open: false,
|
||||||
setOpen: (open) => {
|
setOpen: (open) => {
|
||||||
set({ open })
|
set({ open })
|
||||||
if (!open) set({ pages: [], inputValue: '', cameraScope: null, mode: 'command' })
|
if (!open) set({ pages: [], inputValue: '', mode: 'command' })
|
||||||
},
|
},
|
||||||
mode: 'command',
|
mode: 'command',
|
||||||
setMode: (mode) => set({ mode }),
|
setMode: (mode) => set({ mode }),
|
||||||
@@ -44,12 +42,8 @@ export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
|||||||
setInputValue: (value) => set({ inputValue: value }),
|
setInputValue: (value) => set({ inputValue: value }),
|
||||||
navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })),
|
navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })),
|
||||||
goBack: () => {
|
goBack: () => {
|
||||||
const { pages } = get()
|
|
||||||
if (pages[pages.length - 1] === 'camera-scope') set({ cameraScope: null })
|
|
||||||
set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' }))
|
set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' }))
|
||||||
},
|
},
|
||||||
cameraScope: null,
|
|
||||||
setCameraScope: (scope) => set({ cameraScope: scope }),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -157,8 +151,6 @@ const PAGE_LABEL: Record<string, string> = {
|
|||||||
'level-mode': 'Level Mode',
|
'level-mode': 'Level Mode',
|
||||||
'rename-level': 'Rename Level',
|
'rename-level': 'Rename Level',
|
||||||
'goto-level': 'Go to Level',
|
'goto-level': 'Go to Level',
|
||||||
'camera-view': 'Camera Snapshot',
|
|
||||||
'camera-scope': '',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -196,19 +188,8 @@ function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
|
|||||||
// Main component
|
// Main component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEmptyAction }) {
|
export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEmptyAction }) {
|
||||||
const {
|
const { open, setOpen, mode, setMode, pages, inputValue, setInputValue, navigateTo, goBack } =
|
||||||
open,
|
useCommandPalette()
|
||||||
setOpen,
|
|
||||||
mode,
|
|
||||||
setMode,
|
|
||||||
pages,
|
|
||||||
inputValue,
|
|
||||||
setInputValue,
|
|
||||||
navigateTo,
|
|
||||||
goBack,
|
|
||||||
cameraScope,
|
|
||||||
setCameraScope,
|
|
||||||
} = useCommandPalette()
|
|
||||||
|
|
||||||
const [meta, setMeta] = useState('⌘')
|
const [meta, setMeta] = useState('⌘')
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
@@ -233,11 +214,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const cameraScopeNode = useScene((s) =>
|
|
||||||
cameraScope ? s.nodes[cameraScope.nodeId as AnyNodeId] : null,
|
|
||||||
)
|
|
||||||
const hasScopeSnapshot = !!(cameraScopeNode as any)?.camera
|
|
||||||
|
|
||||||
// Platform detection
|
// Platform detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl')
|
setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl')
|
||||||
@@ -279,7 +255,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|||||||
solo: 'Solo',
|
solo: 'Solo',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Camera snapshot helpers (used by sub-pages registered via EditorCommands)
|
|
||||||
const confirmRename = () => {
|
const confirmRename = () => {
|
||||||
if (!(activeLevelId && inputValue.trim())) return
|
if (!(activeLevelId && inputValue.trim())) return
|
||||||
run(() => {
|
run(() => {
|
||||||
@@ -287,29 +262,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const takeSnapshot = () => {
|
|
||||||
if (!cameraScope) return
|
|
||||||
import('@pascal-app/core').then(({ emitter }) => {
|
|
||||||
run(() =>
|
|
||||||
emitter.emit('camera-controls:capture', { nodeId: cameraScope.nodeId as AnyNodeId }),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewSnapshot = () => {
|
|
||||||
if (!(cameraScope && hasScopeSnapshot)) return
|
|
||||||
import('@pascal-app/core').then(({ emitter }) => {
|
|
||||||
run(() => emitter.emit('camera-controls:view', { nodeId: cameraScope.nodeId as AnyNodeId }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearSnapshot = () => {
|
|
||||||
if (!(cameraScope && hasScopeSnapshot)) return
|
|
||||||
run(() => {
|
|
||||||
useScene.getState().updateNode(cameraScope.nodeId as AnyNodeId, { camera: undefined } as any)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Group registered actions by group (preserving insertion order)
|
// Group registered actions by group (preserving insertion order)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -363,9 +315,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|||||||
onClick={goBack}
|
onClick={goBack}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{page === 'camera-scope'
|
{PAGE_LABEL[page] ?? views.get(page)?.label ?? page}
|
||||||
? (cameraScope?.label ?? 'Snapshot')
|
|
||||||
: (PAGE_LABEL[page] ?? views.get(page)?.label ?? page)}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Command.Input
|
<Command.Input
|
||||||
@@ -500,207 +450,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|||||||
</Command.Item>
|
</Command.Item>
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Camera Snapshot: scope picker ─────────────────────────── */}
|
|
||||||
{page === 'camera-view' && (
|
|
||||||
<Command.Group heading="Camera Snapshot — Select Scope">
|
|
||||||
<OptionItem
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M3 3h18v18H3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
<path d="M3 9h18M9 21V9" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Site"
|
|
||||||
onSelect={() => {
|
|
||||||
const { rootNodeIds } = useScene.getState()
|
|
||||||
const siteId = rootNodeIds[0]
|
|
||||||
if (siteId) {
|
|
||||||
setCameraScope({ nodeId: siteId, label: 'Site' })
|
|
||||||
navigateTo('camera-scope')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<OptionItem
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points="9 22 9 12 15 12 15 22"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Building"
|
|
||||||
onSelect={() => {
|
|
||||||
const building = Object.values(useScene.getState().nodes).find(
|
|
||||||
(n) => n.type === 'building',
|
|
||||||
)
|
|
||||||
if (building) {
|
|
||||||
setCameraScope({ nodeId: building.id, label: 'Building' })
|
|
||||||
navigateTo('camera-scope')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<OptionItem
|
|
||||||
disabled={!activeLevelId}
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 2L2 7l10 5 10-5-10-5z"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M2 17l10 5 10-5M2 12l10 5 10-5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Level"
|
|
||||||
onSelect={() => {
|
|
||||||
if (activeLevelId) {
|
|
||||||
setCameraScope({ nodeId: activeLevelId, label: 'Level' })
|
|
||||||
navigateTo('camera-scope')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<OptionItem
|
|
||||||
disabled={!useViewer.getState().selection.selectedIds.length}
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M5 3l14 9-14 9V3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Selection"
|
|
||||||
onSelect={() => {
|
|
||||||
const firstId = useViewer.getState().selection.selectedIds[0]
|
|
||||||
if (firstId) {
|
|
||||||
setCameraScope({ nodeId: firstId, label: 'Selection' })
|
|
||||||
navigateTo('camera-scope')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Command.Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Camera Snapshot: actions for selected scope ───────────── */}
|
|
||||||
{page === 'camera-scope' && cameraScope && (
|
|
||||||
<Command.Group heading={`${cameraScope.label} Snapshot`}>
|
|
||||||
<OptionItem
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="13"
|
|
||||||
r="4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label={hasScopeSnapshot ? 'Update Snapshot' : 'Take Snapshot'}
|
|
||||||
onSelect={takeSnapshot}
|
|
||||||
/>
|
|
||||||
{hasScopeSnapshot && (
|
|
||||||
<OptionItem
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="3"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="View Snapshot"
|
|
||||||
onSelect={viewSnapshot}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasScopeSnapshot && (
|
|
||||||
<OptionItem
|
|
||||||
icon={
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<polyline
|
|
||||||
points="3 6 5 6 21 6"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M19 6l-1 14H6L5 6"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path d="M10 11v6M14 11v6" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
<path d="M9 6V4h6v2" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Clear Snapshot"
|
|
||||||
onSelect={clearSnapshot}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Command.Group>
|
|
||||||
)}
|
|
||||||
</Command.List>
|
</Command.List>
|
||||||
|
|
||||||
{/* Footer hint */}
|
{/* Footer hint */}
|
||||||
|
|||||||
Executable → Regular
+8
-5
@@ -4,9 +4,11 @@ import {
|
|||||||
getCatalogMaterialById,
|
getCatalogMaterialById,
|
||||||
getLibraryMaterialIdFromRef,
|
getLibraryMaterialIdFromRef,
|
||||||
getMaterialsForCategory,
|
getMaterialsForCategory,
|
||||||
|
getMaterialsForTarget,
|
||||||
MATERIAL_CATEGORIES,
|
MATERIAL_CATEGORIES,
|
||||||
toLibraryMaterialRef,
|
|
||||||
type MaterialSchema,
|
type MaterialSchema,
|
||||||
|
type MaterialTarget,
|
||||||
|
toLibraryMaterialRef,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
@@ -17,6 +19,8 @@ type MaterialPickerProps = {
|
|||||||
onChange?: (material: MaterialSchema) => void
|
onChange?: (material: MaterialSchema) => void
|
||||||
onSelectMaterialPreset?: (materialPreset: string) => void
|
onSelectMaterialPreset?: (materialPreset: string) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
nodeType?: MaterialTarget
|
||||||
|
hideSideControl?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MaterialPicker({
|
export function MaterialPicker({
|
||||||
@@ -48,8 +52,7 @@ export function MaterialPicker({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const catalogId =
|
const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
||||||
getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
|
||||||
const selectedCatalogEntry = getCatalogMaterialById(catalogId)
|
const selectedCatalogEntry = getCatalogMaterialById(catalogId)
|
||||||
if (selectedCatalogEntry?.category) {
|
if (selectedCatalogEntry?.category) {
|
||||||
setSelectedCategory(selectedCatalogEntry.category)
|
setSelectedCategory(selectedCatalogEntry.category)
|
||||||
@@ -177,7 +180,7 @@ export function MaterialPicker({
|
|||||||
title={item.label}
|
title={item.label}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-inset ring-white/12" />
|
<div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-white/12 ring-inset" />
|
||||||
{item.previewThumbnailUrl ? (
|
{item.previewThumbnailUrl ? (
|
||||||
<img
|
<img
|
||||||
alt={item.label}
|
alt={item.label}
|
||||||
@@ -193,7 +196,7 @@ export function MaterialPicker({
|
|||||||
))}
|
))}
|
||||||
{selectedCategory === 'other' && onChange ? (
|
{selectedCategory === 'other' && onChange ? (
|
||||||
<button
|
<button
|
||||||
className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border text-[10px] font-medium transition-all ${
|
className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border font-medium text-[10px] transition-all ${
|
||||||
showCustom
|
showCustom
|
||||||
? 'border-blue-500 ring-2 ring-blue-500/30'
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
||||||
: 'border-gray-300 hover:border-gray-400'
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
|
|||||||
Executable → Regular
+1
-1
@@ -162,7 +162,7 @@ function LevelRow({
|
|||||||
{...dragHandleProps}
|
{...dragHandleProps}
|
||||||
aria-label={`Reorder ${getLevelDisplayLabel(level)}`}
|
aria-label={`Reorder ${getLevelDisplayLabel(level)}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-0.5 flex h-6 w-4 shrink-0 touch-none cursor-grab items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
|
'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
|
||||||
isDragging && 'cursor-grabbing opacity-100',
|
isDragging && 'cursor-grabbing opacity-100',
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useIsMobile } from '../../../hooks/use-mobile'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { BuildingHelper } from './building-helper'
|
import { BuildingHelper } from './building-helper'
|
||||||
import { CeilingHelper } from './ceiling-helper'
|
import { CeilingHelper } from './ceiling-helper'
|
||||||
@@ -12,6 +13,10 @@ export function HelperManager() {
|
|||||||
const mode = useEditor((s) => s.mode)
|
const mode = useEditor((s) => s.mode)
|
||||||
const tool = useEditor((s) => s.tool)
|
const tool = useEditor((s) => s.tool)
|
||||||
const movingNode = useEditor((state) => state.movingNode)
|
const movingNode = useEditor((state) => state.movingNode)
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
// Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
|
||||||
|
if (isMobile) return null
|
||||||
|
|
||||||
if (movingNode) {
|
if (movingNode) {
|
||||||
if (movingNode.type === 'building') return <BuildingHelper showRotate />
|
if (movingNode.type === 'building') return <BuildingHelper showRotate />
|
||||||
|
|||||||
Executable → Regular
+1742
-315
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,53 @@ import { cn } from './../../../lib/utils'
|
|||||||
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
||||||
import { CATALOG_ITEMS } from './catalog-items'
|
import { CATALOG_ITEMS } from './catalog-items'
|
||||||
|
|
||||||
export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
export function ItemCatalog({
|
||||||
|
category,
|
||||||
|
items: itemsOverride,
|
||||||
|
activePlacementTag = null,
|
||||||
|
activeFunctionalTag = null,
|
||||||
|
search = '',
|
||||||
|
overrideItems,
|
||||||
|
leadingTile,
|
||||||
|
emptyState,
|
||||||
|
}: {
|
||||||
|
category: CatalogCategory
|
||||||
|
items?: AssetInput[]
|
||||||
|
activePlacementTag?: string | null
|
||||||
|
activeFunctionalTag?: string | null
|
||||||
|
search?: string
|
||||||
|
/** When set, bypasses all filtering and displays these items directly (used for server search results) */
|
||||||
|
overrideItems?: AssetInput[]
|
||||||
|
/** Rendered as the first grid cell, always visible when there are items. */
|
||||||
|
leadingTile?: React.ReactNode
|
||||||
|
/** Rendered when there are no items to show. Replaces the empty grid. */
|
||||||
|
emptyState?: React.ReactNode
|
||||||
|
}) {
|
||||||
const selectedItem = useEditor((state) => state.selectedItem)
|
const selectedItem = useEditor((state) => state.selectedItem)
|
||||||
const setSelectedItem = useEditor((state) => state.setSelectedItem)
|
const setSelectedItem = useEditor((state) => state.setSelectedItem)
|
||||||
|
const setMode = useEditor((state) => state.setMode)
|
||||||
|
const setTool = useEditor((state) => state.setTool)
|
||||||
|
|
||||||
const categoryItems = CATALOG_ITEMS.filter((item) => item.category === category)
|
const sourceItems = itemsOverride ?? CATALOG_ITEMS
|
||||||
|
// Server-provided results bypass all local filtering; otherwise filter by category/search/tags
|
||||||
|
const filteredItems =
|
||||||
|
overrideItems ??
|
||||||
|
(() => {
|
||||||
|
const categoryItems = search
|
||||||
|
? sourceItems
|
||||||
|
: sourceItems.filter((item) => item.category === category)
|
||||||
|
return categoryItems.filter((item) => {
|
||||||
|
const tags = item.tags ?? []
|
||||||
|
if (activePlacementTag && !tags.includes(activePlacementTag)) return false
|
||||||
|
if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false
|
||||||
|
if (search && !item.name.toLowerCase().includes(search.toLowerCase())) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
// Auto-select first item if current selection is not in this category
|
const categoryItems = filteredItems
|
||||||
|
|
||||||
|
// Auto-select first item if current selection is not in the filtered list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
|
const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
|
||||||
if (!isCurrentItemInCategory && categoryItems.length > 0) {
|
if (!isCurrentItemInCategory && categoryItems.length > 0) {
|
||||||
@@ -27,58 +67,60 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
|||||||
}
|
}
|
||||||
}, [categoryItems, selectedItem?.src, setSelectedItem])
|
}, [categoryItems, selectedItem?.src, setSelectedItem])
|
||||||
|
|
||||||
// Get attachment icon based on attachTo type
|
|
||||||
const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
|
const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
|
||||||
if (attachTo === 'wall' || attachTo === 'wall-side') {
|
if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
|
||||||
return '/icons/wall.png'
|
if (attachTo === 'ceiling') return '/icons/ceiling.png'
|
||||||
}
|
|
||||||
if (attachTo === 'ceiling') {
|
|
||||||
return '/icons/ceiling.png'
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filteredItems.length === 0 && emptyState) {
|
||||||
|
return <>{emptyState}</>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-mx-2 -my-2 flex max-w-xl gap-2 overflow-x-auto p-2">
|
<div
|
||||||
{categoryItems.map((item, index) => {
|
className="grid gap-2"
|
||||||
|
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(90px, 1fr))' }}
|
||||||
|
>
|
||||||
|
{leadingTile}
|
||||||
|
{filteredItems.map((item, index) => {
|
||||||
const isSelected = selectedItem?.src === item?.src
|
const isSelected = selectedItem?.src === item?.src
|
||||||
const attachmentIcon = getAttachmentIcon(item?.attachTo)
|
const attachmentIcon = getAttachmentIcon(item?.attachTo)
|
||||||
return (
|
return (
|
||||||
<Tooltip key={index}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative aspect-square h-14 min-h-14 w-14 min-w-14 shrink-0 flex-col gap-px rounded-lg transition-all duration-200 ease-out hover:scale-105 hover:cursor-pointer',
|
'group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent',
|
||||||
isSelected && 'ring-2 ring-primary-foreground',
|
isSelected && 'bg-sidebar-accent ring-2 ring-primary-foreground',
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedItem(item)}
|
key={index}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItem(item)
|
||||||
|
setTool('item')
|
||||||
|
setMode('build')
|
||||||
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Image
|
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
|
||||||
|
<img
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
className="rounded-lg object-cover"
|
className="h-full w-full object-cover"
|
||||||
fill
|
|
||||||
loading="eager"
|
loading="eager"
|
||||||
sizes="56px"
|
|
||||||
src={resolveCdnUrl(item.thumbnail) || ''}
|
src={resolveCdnUrl(item.thumbnail) || ''}
|
||||||
/>
|
/>
|
||||||
{attachmentIcon && (
|
{attachmentIcon && (
|
||||||
<div className="absolute right-0.5 bottom-0.5 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
<div className="absolute right-1 bottom-1 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
||||||
<Image
|
<img
|
||||||
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
height={16}
|
|
||||||
src={attachmentIcon}
|
src={attachmentIcon}
|
||||||
width={16}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
</TooltipTrigger>
|
<span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
|
||||||
<TooltipContent className="text-xs" side="top">
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</TooltipContent>
|
</span>
|
||||||
</Tooltip>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import type { LevelNode } from '@pascal-app/core'
|
import type { LevelNode } from '@pascal-app/core'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { cn } from '../../lib/utils'
|
|
||||||
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
||||||
|
import { cn } from '../../lib/utils'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -69,9 +69,7 @@ export function LevelDuplicateDialog({
|
|||||||
<DialogContent className="sm:max-w-md" showCloseButton={false}>
|
<DialogContent className="sm:max-w-md" showCloseButton={false}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Duplicate Level</DialogTitle>
|
<DialogTitle>Duplicate Level</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>Choose what to copy from {getLevelLabel(level)}.</DialogDescription>
|
||||||
Choose what to copy from {getLevelLabel(level)}.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
@@ -95,7 +93,7 @@ export function LevelDuplicateDialog({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer rounded-md px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent"
|
className="cursor-pointer rounded-md px-4 py-2 text-muted-foreground text-sm transition-colors hover:bg-accent"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|||||||
Executable → Regular
+2
-3
@@ -120,9 +120,8 @@ export function CeilingPanel() {
|
|||||||
const n = polygon.length
|
const n = polygon.length
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const j = (i + 1) % n
|
const j = (i + 1) % n
|
||||||
const current = polygon[i]
|
const current = polygon[i]!
|
||||||
const next = polygon[j]
|
const next = polygon[j]!
|
||||||
if (!(current && next)) continue
|
|
||||||
area += current[0] * next[1]
|
area += current[0] * next[1]
|
||||||
area -= next[0] * current[1]
|
area -= next[0] * current[1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,10 +70,12 @@ const COLUMN_PROPORTION_PRESETS = {
|
|||||||
|
|
||||||
type ColumnProportionPresetId = keyof typeof COLUMN_PROPORTION_PRESETS
|
type ColumnProportionPresetId = keyof typeof COLUMN_PROPORTION_PRESETS
|
||||||
|
|
||||||
const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(([value, preset]) => ({
|
const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
|
||||||
|
([value, preset]) => ({
|
||||||
value: value as ColumnProportionPresetId,
|
value: value as ColumnProportionPresetId,
|
||||||
label: preset.label,
|
label: preset.label,
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, value))
|
return Math.min(max, Math.max(min, value))
|
||||||
@@ -201,7 +203,12 @@ export function ColumnPanel() {
|
|||||||
const shaftProfile = node.shaftProfile ?? 'straight'
|
const shaftProfile = node.shaftProfile ?? 'straight'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelWrapper icon="/icons/column.png" onClose={handleClose} title={node.name || 'Column'} width={300}>
|
<PanelWrapper
|
||||||
|
icon="/icons/column.png"
|
||||||
|
onClose={handleClose}
|
||||||
|
title={node.name || 'Column'}
|
||||||
|
width={300}
|
||||||
|
>
|
||||||
<PanelSection title="Preset">
|
<PanelSection title="Preset">
|
||||||
<select
|
<select
|
||||||
className={SELECT_CLASS}
|
className={SELECT_CLASS}
|
||||||
@@ -223,7 +230,9 @@ export function ColumnPanel() {
|
|||||||
<PanelSection title="Shape">
|
<PanelSection title="Shape">
|
||||||
<select
|
<select
|
||||||
className={SELECT_CLASS}
|
className={SELECT_CLASS}
|
||||||
onChange={(event) => handleUpdate({ crossSection: event.target.value as ColumnNode['crossSection'] })}
|
onChange={(event) =>
|
||||||
|
handleUpdate({ crossSection: event.target.value as ColumnNode['crossSection'] })
|
||||||
|
}
|
||||||
value={node.crossSection}
|
value={node.crossSection}
|
||||||
>
|
>
|
||||||
<option value="round">Round</option>
|
<option value="round">Round</option>
|
||||||
@@ -313,7 +322,9 @@ export function ColumnPanel() {
|
|||||||
<PanelSection title="Shaft">
|
<PanelSection title="Shaft">
|
||||||
<select
|
<select
|
||||||
className={SELECT_CLASS}
|
className={SELECT_CLASS}
|
||||||
onChange={(event) => handleUpdate(shaftProfileUpdates(event.target.value as ColumnNode['shaftProfile']))}
|
onChange={(event) =>
|
||||||
|
handleUpdate(shaftProfileUpdates(event.target.value as ColumnNode['shaftProfile']))
|
||||||
|
}
|
||||||
value={shaftProfile}
|
value={shaftProfile}
|
||||||
>
|
>
|
||||||
<option value="straight">Straight</option>
|
<option value="straight">Straight</option>
|
||||||
@@ -487,10 +498,22 @@ export function ColumnPanel() {
|
|||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
capitalHeight: Math.max(node.capitalHeight, 0.12),
|
capitalHeight: Math.max(node.capitalHeight, 0.12),
|
||||||
capitalTierCount: capitalStyle === 'stepped' ? Math.max(node.capitalTierCount ?? 3, 3) : node.capitalTierCount,
|
capitalTierCount:
|
||||||
capitalWidthScale: Math.max(node.capitalWidthScale ?? 1.3, capitalStyle === 'stepped' ? 1.42 : 1.28),
|
capitalStyle === 'stepped'
|
||||||
capitalDepthScale: Math.max(node.capitalDepthScale ?? 1.3, capitalStyle === 'stepped' ? 1.42 : 1.28),
|
? Math.max(node.capitalTierCount ?? 3, 3)
|
||||||
capitalStepSpread: capitalStyle === 'stepped' ? Math.max(node.capitalStepSpread ?? 0.34, 0.34) : node.capitalStepSpread,
|
: node.capitalTierCount,
|
||||||
|
capitalWidthScale: Math.max(
|
||||||
|
node.capitalWidthScale ?? 1.3,
|
||||||
|
capitalStyle === 'stepped' ? 1.42 : 1.28,
|
||||||
|
),
|
||||||
|
capitalDepthScale: Math.max(
|
||||||
|
node.capitalDepthScale ?? 1.3,
|
||||||
|
capitalStyle === 'stepped' ? 1.42 : 1.28,
|
||||||
|
),
|
||||||
|
capitalStepSpread:
|
||||||
|
capitalStyle === 'stepped'
|
||||||
|
? Math.max(node.capitalStepSpread ?? 0.34, 0.34)
|
||||||
|
: node.capitalStepSpread,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -572,13 +595,34 @@ export function ColumnPanel() {
|
|||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
baseHeight: Math.max(node.baseHeight, 0.12),
|
baseHeight: Math.max(node.baseHeight, 0.12),
|
||||||
baseTierCount: baseStyle === 'stepped-square' ? Math.max(node.baseTierCount ?? 3, 3) : node.baseTierCount,
|
baseTierCount:
|
||||||
baseWidthScale: Math.max(node.baseWidthScale ?? 1.24, baseStyle === 'stepped-square' ? 1.42 : 1.24),
|
baseStyle === 'stepped-square'
|
||||||
baseDepthScale: Math.max(node.baseDepthScale ?? 1.24, baseStyle === 'stepped-square' ? 1.42 : 1.24),
|
? Math.max(node.baseTierCount ?? 3, 3)
|
||||||
baseStepSpread: baseStyle === 'stepped-square' ? Math.max(node.baseStepSpread ?? 0.34, 0.34) : node.baseStepSpread,
|
: node.baseTierCount,
|
||||||
basePlinthHeightRatio: baseStyle === 'round-rings' ? (node.basePlinthHeightRatio ?? 0.44) : node.basePlinthHeightRatio,
|
baseWidthScale: Math.max(
|
||||||
baseRoundBandScale: baseStyle === 'round-rings' ? (node.baseRoundBandScale ?? 0.92) : node.baseRoundBandScale,
|
node.baseWidthScale ?? 1.24,
|
||||||
baseNeckScale: baseStyle === 'round-rings' ? (node.baseNeckScale ?? 0.72) : node.baseNeckScale,
|
baseStyle === 'stepped-square' ? 1.42 : 1.24,
|
||||||
|
),
|
||||||
|
baseDepthScale: Math.max(
|
||||||
|
node.baseDepthScale ?? 1.24,
|
||||||
|
baseStyle === 'stepped-square' ? 1.42 : 1.24,
|
||||||
|
),
|
||||||
|
baseStepSpread:
|
||||||
|
baseStyle === 'stepped-square'
|
||||||
|
? Math.max(node.baseStepSpread ?? 0.34, 0.34)
|
||||||
|
: node.baseStepSpread,
|
||||||
|
basePlinthHeightRatio:
|
||||||
|
baseStyle === 'round-rings'
|
||||||
|
? (node.basePlinthHeightRatio ?? 0.44)
|
||||||
|
: node.basePlinthHeightRatio,
|
||||||
|
baseRoundBandScale:
|
||||||
|
baseStyle === 'round-rings'
|
||||||
|
? (node.baseRoundBandScale ?? 0.92)
|
||||||
|
: node.baseRoundBandScale,
|
||||||
|
baseNeckScale:
|
||||||
|
baseStyle === 'round-rings'
|
||||||
|
? (node.baseNeckScale ?? 0.72)
|
||||||
|
: node.baseNeckScale,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|||||||
Executable → Regular
+12
-5
@@ -12,8 +12,8 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { BookMarked, Copy, DoorOpen, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
|
import { BookMarked, Copy, DoorOpen, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import { usePresetsAdapter } from '../../../contexts/presets-context'
|
import { usePresetsAdapter } from '../../../contexts/presets-context'
|
||||||
import { cn } from '../../../lib/utils'
|
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
|
import { cn } from '../../../lib/utils'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||||
import { MetricControl } from '../controls/metric-control'
|
import { MetricControl } from '../controls/metric-control'
|
||||||
@@ -148,7 +148,13 @@ export function DoorPanel() {
|
|||||||
const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
|
const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
|
||||||
if (liveNode?.type !== 'door') return
|
if (liveNode?.type !== 'door') return
|
||||||
|
|
||||||
if (!(previewRef.current && previewRef.current.id === selectedId && previewRef.current.key === key)) {
|
if (
|
||||||
|
!(
|
||||||
|
previewRef.current &&
|
||||||
|
previewRef.current.id === selectedId &&
|
||||||
|
previewRef.current.key === key
|
||||||
|
)
|
||||||
|
) {
|
||||||
previewRef.current = {
|
previewRef.current = {
|
||||||
id: selectedId as AnyNodeId,
|
id: selectedId as AnyNodeId,
|
||||||
key,
|
key,
|
||||||
@@ -333,7 +339,8 @@ export function DoorPanel() {
|
|||||||
const normHeights = node.segments.map((seg) => seg.heightRatio / hSum)
|
const normHeights = node.segments.map((seg) => seg.heightRatio / hSum)
|
||||||
const isOpening = node.openingKind === 'opening'
|
const isOpening = node.openingKind === 'opening'
|
||||||
const openingShape = node.openingShape ?? 'rectangle'
|
const openingShape = node.openingShape ?? 'rectangle'
|
||||||
const doorShape = openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
|
const doorShape =
|
||||||
|
openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
|
||||||
const openingRadiusMode = node.openingRadiusMode ?? 'all'
|
const openingRadiusMode = node.openingRadiusMode ?? 'all'
|
||||||
const openingTopRadii = node.openingTopRadii ?? [0.15, 0.15]
|
const openingTopRadii = node.openingTopRadii ?? [0.15, 0.15]
|
||||||
const cornerRadius = node.cornerRadius ?? 0.15
|
const cornerRadius = node.cornerRadius ?? 0.15
|
||||||
@@ -578,7 +585,8 @@ export function DoorPanel() {
|
|||||||
isSelected
|
isSelected
|
||||||
? 'border-orange-400/60 bg-orange-400/10 text-foreground'
|
? 'border-orange-400/60 bg-orange-400/10 text-foreground'
|
||||||
: 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
|
: 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
|
||||||
!option.available && 'cursor-not-allowed opacity-45 hover:bg-[#2C2C2E] hover:text-muted-foreground',
|
!option.available &&
|
||||||
|
'cursor-not-allowed opacity-45 hover:bg-[#2C2C2E] hover:text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
disabled={!option.available}
|
disabled={!option.available}
|
||||||
key={option.value}
|
key={option.value}
|
||||||
@@ -1267,7 +1275,6 @@ export function DoorPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type AnyNode,
|
type AnyNode,
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
@@ -86,10 +85,6 @@ export function FencePanel() {
|
|||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [setSelection])
|
}, [setSelection])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!(node && node.type === 'fence' && selectedId && selectedCount === 1)) return null
|
if (!(node && node.type === 'fence' && selectedId && selectedCount === 1)) return null
|
||||||
|
|
||||||
const length = getWallCurveLength(node)
|
const length = getWallCurveLength(node)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { SliderControl } from '../controls/slider-control'
|
|
||||||
import { Input } from '../primitives/input'
|
|
||||||
import { PanelSection } from '../controls/panel-section'
|
import { PanelSection } from '../controls/panel-section'
|
||||||
|
import { Input } from '../primitives/input'
|
||||||
import { PanelWrapper } from './panel-wrapper'
|
import { PanelWrapper } from './panel-wrapper'
|
||||||
|
|
||||||
function buildDefaultCustomMaterial() {
|
function buildDefaultCustomMaterial() {
|
||||||
@@ -53,11 +52,7 @@ export function PaintPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelWrapper
|
<PanelWrapper onClose={() => setPaintPanelOpen(false)} title="Material" width={320}>
|
||||||
onClose={() => setPaintPanelOpen(false)}
|
|
||||||
title="Material"
|
|
||||||
width={320}
|
|
||||||
>
|
|
||||||
<PanelSection title="Custom Material">
|
<PanelSection title="Custom Material">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -78,42 +73,72 @@ export function PaintPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
<div className="flex items-center justify-between">
|
||||||
Surface
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Roughness
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-1 rounded-lg border border-border/50 bg-background/40 p-2">
|
<span className="font-mono text-muted-foreground text-xs">
|
||||||
<SliderControl
|
{currentProps.roughness.toFixed(2)}
|
||||||
label="Roughness"
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(roughness) => updateCustomMaterial({ roughness })}
|
onChange={(e) =>
|
||||||
precision={2}
|
updateCustomMaterial({ roughness: Number.parseFloat(e.target.value) })
|
||||||
|
}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
type="range"
|
||||||
value={currentProps.roughness}
|
value={currentProps.roughness}
|
||||||
/>
|
/>
|
||||||
<SliderControl
|
</div>
|
||||||
label="Metalness"
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Metalness
|
||||||
|
</label>
|
||||||
|
<span className="font-mono text-muted-foreground text-xs">
|
||||||
|
{currentProps.metalness.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(metalness) => updateCustomMaterial({ metalness })}
|
onChange={(e) =>
|
||||||
precision={2}
|
updateCustomMaterial({ metalness: Number.parseFloat(e.target.value) })
|
||||||
|
}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
type="range"
|
||||||
value={currentProps.metalness}
|
value={currentProps.metalness}
|
||||||
/>
|
/>
|
||||||
<SliderControl
|
</div>
|
||||||
label="Opacity"
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Opacity
|
||||||
|
</label>
|
||||||
|
<span className="font-mono text-muted-foreground text-xs">
|
||||||
|
{currentProps.opacity.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(opacity) =>
|
onChange={(e) => {
|
||||||
|
const opacity = Number.parseFloat(e.target.value)
|
||||||
updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
|
updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
|
||||||
}
|
}}
|
||||||
precision={2}
|
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
type="range"
|
||||||
value={currentProps.opacity}
|
value={currentProps.opacity}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
|||||||
Executable → Regular
+3
-32
@@ -92,6 +92,8 @@ function panelForType(type: string | null) {
|
|||||||
return <StairSegmentPanel />
|
return <StairSegmentPanel />
|
||||||
case 'slab':
|
case 'slab':
|
||||||
return <SlabPanel />
|
return <SlabPanel />
|
||||||
|
case 'spawn':
|
||||||
|
return <SpawnPanel />
|
||||||
case 'ceiling':
|
case 'ceiling':
|
||||||
return <CeilingPanel />
|
return <CeilingPanel />
|
||||||
case 'column':
|
case 'column':
|
||||||
@@ -238,36 +240,5 @@ export function PanelManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show appropriate panel based on selected node type
|
// Show appropriate panel based on selected node type
|
||||||
if (selectedNodeType) {
|
return panelForType(selectedNodeType)
|
||||||
switch (selectedNodeType) {
|
|
||||||
case 'item':
|
|
||||||
return <ItemPanel />
|
|
||||||
case 'roof':
|
|
||||||
return <RoofPanel />
|
|
||||||
case 'roof-segment':
|
|
||||||
return <RoofSegmentPanel />
|
|
||||||
case 'stair':
|
|
||||||
return <StairPanel />
|
|
||||||
case 'stair-segment':
|
|
||||||
return <StairSegmentPanel />
|
|
||||||
case 'slab':
|
|
||||||
return <SlabPanel />
|
|
||||||
case 'spawn':
|
|
||||||
return <SpawnPanel />
|
|
||||||
case 'ceiling':
|
|
||||||
return <CeilingPanel />
|
|
||||||
case 'column':
|
|
||||||
return <ColumnPanel />
|
|
||||||
case 'wall':
|
|
||||||
return <WallPanel />
|
|
||||||
case 'fence':
|
|
||||||
return <FencePanel />
|
|
||||||
case 'door':
|
|
||||||
return <DoorPanel />
|
|
||||||
case 'window':
|
|
||||||
return <WindowPanel />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ import {
|
|||||||
type AnyNode,
|
type AnyNode,
|
||||||
type GuideNode,
|
type GuideNode,
|
||||||
loadAssetUrl,
|
loadAssetUrl,
|
||||||
saveAsset,
|
|
||||||
type ScanNode,
|
type ScanNode,
|
||||||
|
saveAsset,
|
||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { Eye, EyeOff, LocateFixed, Lock, RotateCcw, Ruler, Trash2, Unlock, Upload } from 'lucide-react'
|
import {
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
LocateFixed,
|
||||||
|
Lock,
|
||||||
|
RotateCcw,
|
||||||
|
Ruler,
|
||||||
|
Trash2,
|
||||||
|
Unlock,
|
||||||
|
Upload,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { guideEmitter } from '../../../lib/guide-events'
|
import { guideEmitter } from '../../../lib/guide-events'
|
||||||
import { getGuideImageName } from '../../../lib/local-guide-image'
|
import { getGuideImageName } from '../../../lib/local-guide-image'
|
||||||
@@ -32,7 +42,9 @@ function getScaleStatus(guide: GuideNode, scaleReferenceVisible: boolean) {
|
|||||||
export function ReferencePanel() {
|
export function ReferencePanel() {
|
||||||
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
||||||
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
||||||
const guideUi = useEditor((s) => (selectedReferenceId ? s.guideUi[selectedReferenceId] : undefined))
|
const guideUi = useEditor((s) =>
|
||||||
|
selectedReferenceId ? s.guideUi[selectedReferenceId] : undefined,
|
||||||
|
)
|
||||||
const setGuideLocked = useEditor((s) => s.setGuideLocked)
|
const setGuideLocked = useEditor((s) => s.setGuideLocked)
|
||||||
const setGuideScaleReferenceVisible = useEditor((s) => s.setGuideScaleReferenceVisible)
|
const setGuideScaleReferenceVisible = useEditor((s) => s.setGuideScaleReferenceVisible)
|
||||||
const clearGuideUi = useEditor((s) => s.clearGuideUi)
|
const clearGuideUi = useEditor((s) => s.clearGuideUi)
|
||||||
@@ -77,11 +89,14 @@ export function ReferencePanel() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const assetUrl = await saveAsset(file)
|
const assetUrl = await saveAsset(file)
|
||||||
updateNode(selectedReferenceId as AnyNode['id'], {
|
updateNode(
|
||||||
|
selectedReferenceId as AnyNode['id'],
|
||||||
|
{
|
||||||
name: getGuideImageName(file.name),
|
name: getGuideImageName(file.name),
|
||||||
url: assetUrl,
|
url: assetUrl,
|
||||||
scaleReference: null,
|
scaleReference: null,
|
||||||
} as Partial<GuideNode>)
|
} as Partial<GuideNode>,
|
||||||
|
)
|
||||||
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
||||||
} catch {
|
} catch {
|
||||||
setReplaceError('Could not replace that image.')
|
setReplaceError('Could not replace that image.')
|
||||||
@@ -138,7 +153,7 @@ export function ReferencePanel() {
|
|||||||
const isScan = node.type === 'scan'
|
const isScan = node.type === 'scan'
|
||||||
const guideLocked = !isScan && guideUi?.locked === true
|
const guideLocked = !isScan && guideUi?.locked === true
|
||||||
const scaleReferenceVisible = !isScan && guideUi?.scaleReferenceVisible !== false
|
const scaleReferenceVisible = !isScan && guideUi?.scaleReferenceVisible !== false
|
||||||
const scaleStatus = !isScan ? getScaleStatus(node, scaleReferenceVisible) : null
|
const scaleStatus = isScan ? null : getScaleStatus(node, scaleReferenceVisible)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelWrapper
|
<PanelWrapper
|
||||||
@@ -165,16 +180,16 @@ export function ReferencePanel() {
|
|||||||
|
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
disabled={isReplacing}
|
||||||
icon={<Upload className="h-3.5 w-3.5" />}
|
icon={<Upload className="h-3.5 w-3.5" />}
|
||||||
label={isReplacing ? 'Replacing...' : 'Replace'}
|
label={isReplacing ? 'Replacing...' : 'Replace'}
|
||||||
onClick={() => replaceInputRef.current?.click()}
|
onClick={() => replaceInputRef.current?.click()}
|
||||||
disabled={isReplacing}
|
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className="text-destructive hover:bg-destructive/10"
|
||||||
icon={<Trash2 className="h-3.5 w-3.5" />}
|
icon={<Trash2 className="h-3.5 w-3.5" />}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
onClick={handleDeleteGuide}
|
onClick={handleDeleteGuide}
|
||||||
className="text-destructive hover:bg-destructive/10"
|
|
||||||
/>
|
/>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
|
|
||||||
@@ -232,16 +247,16 @@ export function ReferencePanel() {
|
|||||||
|
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
|
||||||
disabled={!node.scaleReference}
|
disabled={!node.scaleReference}
|
||||||
|
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!node.scaleReference) return
|
if (!node.scaleReference) return
|
||||||
setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)
|
setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label="Clear Scale"
|
|
||||||
disabled={!node.scaleReference}
|
disabled={!node.scaleReference}
|
||||||
|
label="Clear Scale"
|
||||||
onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
|
onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
|
||||||
/>
|
/>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
|
|||||||
Executable → Regular
+52
-2
@@ -3,21 +3,25 @@
|
|||||||
import {
|
import {
|
||||||
type AnyNode,
|
type AnyNode,
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
|
getEffectiveRoofSurfaceMaterial,
|
||||||
|
type MaterialSchema,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
type RoofSurfaceMaterialRole,
|
|
||||||
RoofNode as RoofNodeSchema,
|
RoofNode as RoofNodeSchema,
|
||||||
type RoofSegmentNode,
|
type RoofSegmentNode,
|
||||||
RoofSegmentNode as RoofSegmentNodeSchema,
|
RoofSegmentNode as RoofSegmentNodeSchema,
|
||||||
|
type RoofSurfaceMaterialRole,
|
||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { buildRoofSurfaceMaterialPatch } from '../../../lib/material-paint'
|
||||||
import { duplicateRoofSubtree } from '../../../lib/roof-duplication'
|
import { duplicateRoofSubtree } from '../../../lib/roof-duplication'
|
||||||
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||||
|
import { MaterialPicker } from '../controls/material-picker'
|
||||||
import { PanelSection } from '../controls/panel-section'
|
import { PanelSection } from '../controls/panel-section'
|
||||||
import { SliderControl } from '../controls/slider-control'
|
import { SliderControl } from '../controls/slider-control'
|
||||||
import { PanelWrapper } from './panel-wrapper'
|
import { PanelWrapper } from './panel-wrapper'
|
||||||
@@ -28,6 +32,7 @@ export function RoofPanel() {
|
|||||||
const updateNode = useScene((s) => s.updateNode)
|
const updateNode = useScene((s) => s.updateNode)
|
||||||
const createNode = useScene((s) => s.createNode)
|
const createNode = useScene((s) => s.createNode)
|
||||||
const setMovingNode = useEditor((s) => s.setMovingNode)
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
||||||
|
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
||||||
|
|
||||||
const node = useScene((s) =>
|
const node = useScene((s) =>
|
||||||
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
|
||||||
@@ -50,6 +55,35 @@ export function RoofPanel() {
|
|||||||
[selectedId, updateNode],
|
[selectedId, updateNode],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const materialTargetRole =
|
||||||
|
selectedMaterialTarget &&
|
||||||
|
selectedMaterialTarget.nodeId === node?.id &&
|
||||||
|
(selectedMaterialTarget.role === 'top' ||
|
||||||
|
selectedMaterialTarget.role === 'edge' ||
|
||||||
|
selectedMaterialTarget.role === 'wall')
|
||||||
|
? selectedMaterialTarget.role
|
||||||
|
: null
|
||||||
|
const materialPickerValue =
|
||||||
|
node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {}
|
||||||
|
|
||||||
|
const handleTargetedMaterialChange = useCallback(
|
||||||
|
(material: MaterialSchema) => {
|
||||||
|
if (!(node && materialTargetRole)) return
|
||||||
|
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
||||||
|
},
|
||||||
|
[handleUpdate, materialTargetRole, node],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTargetedMaterialPresetChange = useCallback(
|
||||||
|
(materialPreset: string) => {
|
||||||
|
if (!(node && materialTargetRole)) return
|
||||||
|
handleUpdate(
|
||||||
|
buildRoofSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[handleUpdate, materialTargetRole, node],
|
||||||
|
)
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [setSelection])
|
}, [setSelection])
|
||||||
@@ -225,6 +259,22 @@ export function RoofPanel() {
|
|||||||
/>
|
/>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
<PanelSection title="Material">
|
||||||
|
{materialTargetRole ? null : (
|
||||||
|
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
||||||
|
Click the roof surface you want to edit. Materials apply to one target at a time.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<MaterialPicker
|
||||||
|
disabled={!materialTargetRole}
|
||||||
|
hideSideControl
|
||||||
|
nodeType="roof"
|
||||||
|
onChange={handleTargetedMaterialChange}
|
||||||
|
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
||||||
|
selectedMaterialPreset={materialPickerValue.materialPreset}
|
||||||
|
value={materialPickerValue.material}
|
||||||
|
/>
|
||||||
|
</PanelSection>
|
||||||
</PanelWrapper>
|
</PanelWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
@@ -87,7 +87,7 @@ export function SpawnPanel() {
|
|||||||
|
|
||||||
if (!(node && node.type === 'spawn' && selectedId)) return null
|
if (!(node && node.type === 'spawn' && selectedId)) return null
|
||||||
|
|
||||||
const rotationDegrees = Math.round((((draftRotation ?? node.rotation) * 180) / Math.PI))
|
const rotationDegrees = Math.round(((draftRotation ?? node.rotation) * 180) / Math.PI)
|
||||||
const storedRotationDegrees = Math.round((node.rotation * 180) / Math.PI)
|
const storedRotationDegrees = Math.round((node.rotation * 180) / Math.PI)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,7 +97,9 @@ export function SpawnPanel() {
|
|||||||
label="X"
|
label="X"
|
||||||
max={node.position[0] + 2}
|
max={node.position[0] + 2}
|
||||||
min={node.position[0] - 2}
|
min={node.position[0] - 2}
|
||||||
onChange={(value) => handleUpdate({ position: [value, node.position[1], node.position[2]] })}
|
onChange={(value) =>
|
||||||
|
handleUpdate({ position: [value, node.position[1], node.position[2]] })
|
||||||
|
}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
unit="m"
|
unit="m"
|
||||||
@@ -107,7 +109,9 @@ export function SpawnPanel() {
|
|||||||
label="Y"
|
label="Y"
|
||||||
max={node.position[1] + 2}
|
max={node.position[1] + 2}
|
||||||
min={node.position[1] - 2}
|
min={node.position[1] - 2}
|
||||||
onChange={(value) => handleUpdate({ position: [node.position[0], value, node.position[2]] })}
|
onChange={(value) =>
|
||||||
|
handleUpdate({ position: [node.position[0], value, node.position[2]] })
|
||||||
|
}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
unit="m"
|
unit="m"
|
||||||
@@ -117,7 +121,9 @@ export function SpawnPanel() {
|
|||||||
label="Z"
|
label="Z"
|
||||||
max={node.position[2] + 2}
|
max={node.position[2] + 2}
|
||||||
min={node.position[2] - 2}
|
min={node.position[2] - 2}
|
||||||
onChange={(value) => handleUpdate({ position: [node.position[0], node.position[1], value] })}
|
onChange={(value) =>
|
||||||
|
handleUpdate({ position: [node.position[0], node.position[1], value] })
|
||||||
|
}
|
||||||
precision={2}
|
precision={2}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
unit="m"
|
unit="m"
|
||||||
|
|||||||
@@ -3,25 +3,31 @@
|
|||||||
import {
|
import {
|
||||||
type AnyNode,
|
type AnyNode,
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
|
getEffectiveStairSurfaceMaterial,
|
||||||
type LevelNode,
|
type LevelNode,
|
||||||
|
type MaterialSchema,
|
||||||
type StairNode,
|
type StairNode,
|
||||||
|
StairNode as StairNodeSchema,
|
||||||
type StairRailingMode,
|
type StairRailingMode,
|
||||||
type StairSlabOpeningMode,
|
|
||||||
type StairTopLandingMode,
|
|
||||||
type StairType,
|
|
||||||
type StairSegmentNode,
|
type StairSegmentNode,
|
||||||
StairSegmentNode as StairSegmentNodeSchema,
|
StairSegmentNode as StairSegmentNodeSchema,
|
||||||
|
type StairSlabOpeningMode,
|
||||||
|
type StairSurfaceMaterialRole,
|
||||||
|
type StairTopLandingMode,
|
||||||
|
type StairType,
|
||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { duplicateStairSubtree } from '../../../lib/stair-duplication'
|
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import { buildStairSurfaceMaterialPatch } from '../../../lib/material-paint'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
|
import { duplicateStairSubtree } from '../../../lib/stair-duplication'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
|
import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
|
||||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||||
|
import { MaterialPicker } from '../controls/material-picker'
|
||||||
import { MetricControl } from '../controls/metric-control'
|
import { MetricControl } from '../controls/metric-control'
|
||||||
import { PanelSection } from '../controls/panel-section'
|
import { PanelSection } from '../controls/panel-section'
|
||||||
import { SegmentedControl } from '../controls/segmented-control'
|
import { SegmentedControl } from '../controls/segmented-control'
|
||||||
@@ -59,6 +65,7 @@ export function StairPanel() {
|
|||||||
const updateNode = useScene((s) => s.updateNode)
|
const updateNode = useScene((s) => s.updateNode)
|
||||||
const createNode = useScene((s) => s.createNode)
|
const createNode = useScene((s) => s.createNode)
|
||||||
const setMovingNode = useEditor((s) => s.setMovingNode)
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
||||||
|
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
||||||
|
|
||||||
const node = useScene((s) =>
|
const node = useScene((s) =>
|
||||||
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
|
||||||
@@ -89,6 +96,35 @@ export function StairPanel() {
|
|||||||
[selectedId, updateNode],
|
[selectedId, updateNode],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const materialTargetRole =
|
||||||
|
selectedMaterialTarget &&
|
||||||
|
selectedMaterialTarget.nodeId === node?.id &&
|
||||||
|
(selectedMaterialTarget.role === 'railing' ||
|
||||||
|
selectedMaterialTarget.role === 'tread' ||
|
||||||
|
selectedMaterialTarget.role === 'side')
|
||||||
|
? selectedMaterialTarget.role
|
||||||
|
: null
|
||||||
|
const materialPickerValue =
|
||||||
|
node && materialTargetRole ? getEffectiveStairSurfaceMaterial(node, materialTargetRole) : {}
|
||||||
|
|
||||||
|
const handleTargetedMaterialChange = useCallback(
|
||||||
|
(material: MaterialSchema) => {
|
||||||
|
if (!(node && materialTargetRole)) return
|
||||||
|
handleUpdate(buildStairSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
||||||
|
},
|
||||||
|
[handleUpdate, materialTargetRole, node],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTargetedMaterialPresetChange = useCallback(
|
||||||
|
(materialPreset: string) => {
|
||||||
|
if (!(node && materialTargetRole)) return
|
||||||
|
handleUpdate(
|
||||||
|
buildStairSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[handleUpdate, materialTargetRole, node],
|
||||||
|
)
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [setSelection])
|
}, [setSelection])
|
||||||
@@ -222,11 +258,11 @@ export function StairPanel() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
<div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
|
||||||
From Level
|
From Level
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
|
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-foreground text-sm"
|
||||||
onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
|
onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
|
||||||
value={resolvedFromLevelId ?? ''}
|
value={resolvedFromLevelId ?? ''}
|
||||||
>
|
>
|
||||||
@@ -239,11 +275,11 @@ export function StairPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
<div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
|
||||||
To Level
|
To Level
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
|
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-foreground text-sm"
|
||||||
onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
|
onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
|
||||||
value={resolvedToLevelId ?? ''}
|
value={resolvedToLevelId ?? ''}
|
||||||
>
|
>
|
||||||
@@ -262,7 +298,7 @@ export function StairPanel() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{(node.slabOpeningMode ?? 'none') === 'destination' ? (
|
{(node.slabOpeningMode ?? 'none') === 'destination' ? (
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Opening Offset"
|
label="Opening Offset"
|
||||||
max={0.5}
|
max={0.5}
|
||||||
min={0}
|
min={0}
|
||||||
@@ -308,7 +344,7 @@ export function StairPanel() {
|
|||||||
|
|
||||||
{(node.stairType === 'curved' || node.stairType === 'spiral') && (
|
{(node.stairType === 'curved' || node.stairType === 'spiral') && (
|
||||||
<PanelSection title="Geometry">
|
<PanelSection title="Geometry">
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Width"
|
label="Width"
|
||||||
max={10}
|
max={10}
|
||||||
min={0.4}
|
min={0.4}
|
||||||
@@ -318,7 +354,7 @@ export function StairPanel() {
|
|||||||
unit="m"
|
unit="m"
|
||||||
value={Math.round((node.width ?? 1) * 100) / 100}
|
value={Math.round((node.width ?? 1) * 100) / 100}
|
||||||
/>
|
/>
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Rise"
|
label="Rise"
|
||||||
max={10}
|
max={10}
|
||||||
min={0.2}
|
min={0.2}
|
||||||
@@ -328,7 +364,7 @@ export function StairPanel() {
|
|||||||
unit="m"
|
unit="m"
|
||||||
value={Math.round((node.totalRise ?? 2.5) * 100) / 100}
|
value={Math.round((node.totalRise ?? 2.5) * 100) / 100}
|
||||||
/>
|
/>
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Steps"
|
label="Steps"
|
||||||
max={32}
|
max={32}
|
||||||
min={2}
|
min={2}
|
||||||
@@ -346,7 +382,7 @@ export function StairPanel() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && (
|
{(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && (
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Thickness"
|
label="Thickness"
|
||||||
max={1}
|
max={1}
|
||||||
min={0.02}
|
min={0.02}
|
||||||
@@ -357,7 +393,7 @@ export function StairPanel() {
|
|||||||
value={Math.round((node.thickness ?? 0.25) * 100) / 100}
|
value={Math.round((node.thickness ?? 0.25) * 100) / 100}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Inner Radius"
|
label="Inner Radius"
|
||||||
max={10}
|
max={10}
|
||||||
min={node.stairType === 'spiral' ? 0.05 : 0.2}
|
min={node.stairType === 'spiral' ? 0.05 : 0.2}
|
||||||
@@ -385,7 +421,7 @@ export function StairPanel() {
|
|||||||
value={node.topLandingMode ?? 'none'}
|
value={node.topLandingMode ?? 'none'}
|
||||||
/>
|
/>
|
||||||
{(node.topLandingMode ?? 'none') === 'integrated' && (
|
{(node.topLandingMode ?? 'none') === 'integrated' && (
|
||||||
<SliderControl
|
<MetricControl
|
||||||
label="Top Landing"
|
label="Top Landing"
|
||||||
max={5}
|
max={5}
|
||||||
min={0.3}
|
min={0.3}
|
||||||
@@ -520,6 +556,22 @@ export function StairPanel() {
|
|||||||
/>
|
/>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
<PanelSection title="Material">
|
||||||
|
{materialTargetRole ? null : (
|
||||||
|
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
||||||
|
Click the stair surface you want to edit. Materials apply to one target at a time.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<MaterialPicker
|
||||||
|
disabled={!materialTargetRole}
|
||||||
|
hideSideControl
|
||||||
|
nodeType="stair"
|
||||||
|
onChange={handleTargetedMaterialChange}
|
||||||
|
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
||||||
|
selectedMaterialPreset={materialPickerValue.materialPreset}
|
||||||
|
value={materialPickerValue.material}
|
||||||
|
/>
|
||||||
|
</PanelSection>
|
||||||
</PanelWrapper>
|
</PanelWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+97
-1
@@ -4,28 +4,60 @@ import {
|
|||||||
type AnyNode,
|
type AnyNode,
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
getClampedWallCurveOffset,
|
getClampedWallCurveOffset,
|
||||||
|
getEffectiveWallSurfaceMaterial,
|
||||||
getMaxWallCurveOffset,
|
getMaxWallCurveOffset,
|
||||||
getWallCurveLength,
|
getWallCurveLength,
|
||||||
|
getWallSurfaceMaterialSignature,
|
||||||
|
type MaterialSchema,
|
||||||
normalizeWallCurveOffset,
|
normalizeWallCurveOffset,
|
||||||
useScene,
|
useScene,
|
||||||
type WallNode,
|
type WallNode,
|
||||||
|
type WallSurfaceSide,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { Move, Spline } from 'lucide-react'
|
import { Move, Spline } from 'lucide-react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } 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'
|
||||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||||
|
import { MaterialPicker } from '../controls/material-picker'
|
||||||
import { PanelSection } from '../controls/panel-section'
|
import { PanelSection } from '../controls/panel-section'
|
||||||
import { SliderControl } from '../controls/slider-control'
|
import { SliderControl } from '../controls/slider-control'
|
||||||
import { PanelWrapper } from './panel-wrapper'
|
import { PanelWrapper } from './panel-wrapper'
|
||||||
|
|
||||||
|
function buildWallSurfaceMaterialPatch(
|
||||||
|
node: WallNode,
|
||||||
|
targetSide: WallSurfaceSide | null,
|
||||||
|
material: MaterialSchema | undefined,
|
||||||
|
materialPreset: string | undefined,
|
||||||
|
): Partial<WallNode> {
|
||||||
|
const nextSurfaceMaterial = { material, materialPreset }
|
||||||
|
const nextInterior =
|
||||||
|
targetSide === null || targetSide === 'interior'
|
||||||
|
? nextSurfaceMaterial
|
||||||
|
: getEffectiveWallSurfaceMaterial(node, 'interior')
|
||||||
|
const nextExterior =
|
||||||
|
targetSide === null || targetSide === 'exterior'
|
||||||
|
? nextSurfaceMaterial
|
||||||
|
: getEffectiveWallSurfaceMaterial(node, 'exterior')
|
||||||
|
|
||||||
|
return {
|
||||||
|
interiorMaterial: nextInterior.material,
|
||||||
|
interiorMaterialPreset: nextInterior.materialPreset,
|
||||||
|
exteriorMaterial: nextExterior.material,
|
||||||
|
exteriorMaterialPreset: nextExterior.materialPreset,
|
||||||
|
material: undefined,
|
||||||
|
materialPreset: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function WallPanel() {
|
export function WallPanel() {
|
||||||
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
||||||
const setSelection = useViewer((s) => s.setSelection)
|
const setSelection = useViewer((s) => s.setSelection)
|
||||||
const updateNode = useScene((s) => s.updateNode)
|
const updateNode = useScene((s) => s.updateNode)
|
||||||
const setMovingNode = useEditor((s) => s.setMovingNode)
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
||||||
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
||||||
|
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
||||||
|
|
||||||
const node = useScene((s) =>
|
const node = useScene((s) =>
|
||||||
selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
|
||||||
@@ -56,6 +88,35 @@ export function WallPanel() {
|
|||||||
[selectedId, updateNode],
|
[selectedId, updateNode],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const effectiveInteriorMaterial = useMemo(
|
||||||
|
() => (node ? getEffectiveWallSurfaceMaterial(node, 'interior') : {}),
|
||||||
|
[node],
|
||||||
|
)
|
||||||
|
const effectiveExteriorMaterial = useMemo(
|
||||||
|
() => (node ? getEffectiveWallSurfaceMaterial(node, 'exterior') : {}),
|
||||||
|
[node],
|
||||||
|
)
|
||||||
|
const surfaceMaterialsMatch = useMemo(
|
||||||
|
() =>
|
||||||
|
getWallSurfaceMaterialSignature(effectiveInteriorMaterial) ===
|
||||||
|
getWallSurfaceMaterialSignature(effectiveExteriorMaterial),
|
||||||
|
[effectiveExteriorMaterial, effectiveInteriorMaterial],
|
||||||
|
)
|
||||||
|
const materialTargetSide =
|
||||||
|
selectedMaterialTarget &&
|
||||||
|
selectedMaterialTarget.nodeId === node?.id &&
|
||||||
|
(selectedMaterialTarget.role === 'interior' || selectedMaterialTarget.role === 'exterior')
|
||||||
|
? selectedMaterialTarget.role
|
||||||
|
: null
|
||||||
|
const materialPickerValue =
|
||||||
|
materialTargetSide === 'interior'
|
||||||
|
? effectiveInteriorMaterial
|
||||||
|
: materialTargetSide === 'exterior'
|
||||||
|
? effectiveExteriorMaterial
|
||||||
|
: surfaceMaterialsMatch
|
||||||
|
? effectiveInteriorMaterial
|
||||||
|
: {}
|
||||||
|
|
||||||
const handleUpdateLength = useCallback(
|
const handleUpdateLength = useCallback(
|
||||||
(newLength: number) => {
|
(newLength: number) => {
|
||||||
if (!node || newLength <= 0) return
|
if (!node || newLength <= 0) return
|
||||||
@@ -79,6 +140,24 @@ export function WallPanel() {
|
|||||||
[node, handleUpdate],
|
[node, handleUpdate],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleMaterialPresetChange = useCallback(
|
||||||
|
(materialPreset: string) => {
|
||||||
|
if (!(node && materialTargetSide)) return
|
||||||
|
handleUpdate(
|
||||||
|
buildWallSurfaceMaterialPatch(node, materialTargetSide, undefined, materialPreset),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[handleUpdate, materialTargetSide, node],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCustomMaterialChange = useCallback(
|
||||||
|
(material: MaterialSchema) => {
|
||||||
|
if (!(node && materialTargetSide)) return
|
||||||
|
handleUpdate(buildWallSurfaceMaterialPatch(node, materialTargetSide, material, undefined))
|
||||||
|
},
|
||||||
|
[handleUpdate, materialTargetSide, node],
|
||||||
|
)
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [setSelection])
|
}, [setSelection])
|
||||||
@@ -160,6 +239,23 @@ export function WallPanel() {
|
|||||||
)}
|
)}
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
|
||||||
|
<PanelSection title="Material">
|
||||||
|
{materialTargetSide ? null : (
|
||||||
|
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
||||||
|
Click the wall face you want to edit. Materials now apply to one side at a time.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<MaterialPicker
|
||||||
|
disabled={!materialTargetSide}
|
||||||
|
hideSideControl
|
||||||
|
nodeType="wall"
|
||||||
|
onChange={handleCustomMaterialChange}
|
||||||
|
onSelectMaterialPreset={handleMaterialPresetChange}
|
||||||
|
selectedMaterialPreset={materialPickerValue.materialPreset}
|
||||||
|
value={materialPickerValue.material}
|
||||||
|
/>
|
||||||
|
</PanelSection>
|
||||||
|
|
||||||
<PanelSection title="Actions">
|
<PanelSection title="Actions">
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user