Merge pull request #85 from pascalorg/chore/npm-packages
Chore/npm packages
This commit is contained in:
@@ -26,6 +26,7 @@ coverage
|
|||||||
out/
|
out/
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
|
|||||||
@@ -1,135 +1,397 @@
|
|||||||
# Turborepo starter
|
# Pascal Editor
|
||||||
|
|
||||||
This Turborepo starter is maintained by the Turborepo core team.
|
A 3D building editor built with React Three Fiber and WebGPU.
|
||||||
|
|
||||||
## Using this example
|
## Repository Architecture
|
||||||
|
|
||||||
Run the following command:
|
This is a Turborepo monorepo with three main packages:
|
||||||
|
|
||||||
```sh
|
|
||||||
npx create-turbo@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## What's inside?
|
|
||||||
|
|
||||||
This Turborepo includes the following packages/apps:
|
|
||||||
|
|
||||||
### Apps and Packages
|
|
||||||
|
|
||||||
- `docs`: a [Next.js](https://nextjs.org/) app
|
|
||||||
- `web`: another [Next.js](https://nextjs.org/) app
|
|
||||||
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
|
|
||||||
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
|
|
||||||
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
|
|
||||||
|
|
||||||
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
|
||||||
|
|
||||||
### Utilities
|
|
||||||
|
|
||||||
This Turborepo has some additional tools already setup for you:
|
|
||||||
|
|
||||||
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
|
||||||
- [ESLint](https://eslint.org/) for code linting
|
|
||||||
- [Prettier](https://prettier.io) for code formatting
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
To build all apps and packages, run the following command:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd my-turborepo
|
editor-v2/
|
||||||
|
├── apps/
|
||||||
|
│ └── editor/ # Next.js application
|
||||||
|
├── packages/
|
||||||
|
│ ├── core/ # Schema definitions, state management, systems
|
||||||
|
│ └── viewer/ # 3D rendering components
|
||||||
|
```
|
||||||
|
|
||||||
# With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended)
|
### Separation of Concerns
|
||||||
|
|
||||||
|
| Package | Responsibility |
|
||||||
|
|---------|---------------|
|
||||||
|
| **@pascal-app/core** | Node schemas, scene state (Zustand), systems (geometry generation), spatial queries, event bus |
|
||||||
|
| **@pascal-app/viewer** | 3D rendering via React Three Fiber, default camera/controls, post-processing |
|
||||||
|
| **apps/editor** | UI components, tools, custom behaviors, editor-specific systems |
|
||||||
|
|
||||||
|
The **viewer** renders the scene with sensible defaults. The **editor** extends it with interactive tools, selection management, and editing capabilities.
|
||||||
|
|
||||||
|
### Stores
|
||||||
|
|
||||||
|
Each package has its own Zustand store for managing state:
|
||||||
|
|
||||||
|
| Store | Package | Responsibility |
|
||||||
|
|-------|---------|----------------|
|
||||||
|
| `useScene` | `@pascal-app/core` | Scene data: nodes, root IDs, dirty nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo. |
|
||||||
|
| `useViewer` | `@pascal-app/viewer` | Viewer state: current selection (building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode. |
|
||||||
|
| `useEditor` | `apps/editor` | Editor state: active tool, structure layer visibility, panel states, editor-specific preferences. |
|
||||||
|
|
||||||
|
**Access patterns:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Subscribe to state changes (React component)
|
||||||
|
const nodes = useScene((state) => state.nodes)
|
||||||
|
const levelId = useViewer((state) => state.selection.levelId)
|
||||||
|
const activeTool = useEditor((state) => state.tool)
|
||||||
|
|
||||||
|
// Access state outside React (callbacks, systems)
|
||||||
|
const node = useScene.getState().nodes[id]
|
||||||
|
useViewer.getState().setSelection({ levelId: 'level_123' })
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Nodes
|
||||||
|
|
||||||
|
Nodes are the data primitives that describe the 3D scene. All nodes extend `BaseNode`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
BaseNode {
|
||||||
|
id: string // Auto-generated with type prefix (e.g., "wall_abc123")
|
||||||
|
type: string // Discriminator for type-safe handling
|
||||||
|
parentId: string | null // Parent node reference
|
||||||
|
visible: boolean
|
||||||
|
camera?: Camera // Optional saved camera position
|
||||||
|
metadata?: JSON // Arbitrary metadata (e.g., { isTransient: true })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node Hierarchy:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Site
|
||||||
|
└── Building
|
||||||
|
└── Level
|
||||||
|
├── Wall → Item (doors, windows)
|
||||||
|
├── Slab
|
||||||
|
├── Ceiling → Item (lights)
|
||||||
|
├── Roof
|
||||||
|
├── Zone
|
||||||
|
├── Scan (3D reference)
|
||||||
|
└── Guide (2D reference)
|
||||||
|
```
|
||||||
|
|
||||||
|
Nodes are stored in a **flat dictionary** (`Record<id, Node>`), not a nested tree. Parent-child relationships are defined via `parentId` and `children` arrays.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scene State (Zustand Store)
|
||||||
|
|
||||||
|
The scene is managed by a Zustand store in `@pascal-app/core`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useScene.getState() = {
|
||||||
|
nodes: Record<id, AnyNode>, // All nodes
|
||||||
|
rootNodeIds: string[], // Top-level nodes (sites)
|
||||||
|
dirtyNodes: Set<string>, // Nodes pending system updates
|
||||||
|
|
||||||
|
createNode(node, parentId),
|
||||||
|
updateNode(id, updates),
|
||||||
|
deleteNode(id),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Middleware:**
|
||||||
|
- **Persist** - Saves to IndexedDB (excludes transient nodes)
|
||||||
|
- **Temporal** (Zundo) - Undo/redo with 50-step history
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scene Registry
|
||||||
|
|
||||||
|
The registry maps node IDs to their Three.js objects for fast lookup:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
sceneRegistry = {
|
||||||
|
nodes: Map<id, Object3D>, // ID → 3D object
|
||||||
|
byType: {
|
||||||
|
wall: Set<id>,
|
||||||
|
item: Set<id>,
|
||||||
|
zone: Set<id>,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Renderers register their refs using the `useRegistry` hook:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const ref = useRef<Mesh>(null!)
|
||||||
|
useRegistry(node.id, 'wall', ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows systems to access 3D objects directly without traversing the scene graph.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Node Renderers
|
||||||
|
|
||||||
|
Renderers are React components that create Three.js objects for each node type:
|
||||||
|
|
||||||
|
```
|
||||||
|
SceneRenderer
|
||||||
|
└── NodeRenderer (dispatches by type)
|
||||||
|
├── BuildingRenderer
|
||||||
|
├── LevelRenderer
|
||||||
|
├── WallRenderer
|
||||||
|
├── SlabRenderer
|
||||||
|
├── ZoneRenderer
|
||||||
|
├── ItemRenderer
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
1. Renderer creates a placeholder mesh/group
|
||||||
|
2. Registers it with `useRegistry`
|
||||||
|
3. Systems update geometry based on node data
|
||||||
|
|
||||||
|
Example (simplified):
|
||||||
|
```tsx
|
||||||
|
const WallRenderer = ({ node }) => {
|
||||||
|
const ref = useRef<Mesh>(null!)
|
||||||
|
useRegistry(node.id, 'wall', ref)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh ref={ref}>
|
||||||
|
<boxGeometry args={[0, 0, 0]} /> {/* Replaced by WallSystem */}
|
||||||
|
<meshStandardMaterial />
|
||||||
|
{node.children.map(id => <NodeRenderer key={id} nodeId={id} />)}
|
||||||
|
</mesh>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Systems
|
||||||
|
|
||||||
|
Systems are React components that run in the render loop (`useFrame`) to update geometry and transforms. They process **dirty nodes** marked by the store.
|
||||||
|
|
||||||
|
**Core Systems (in `@pascal-app/core`):**
|
||||||
|
|
||||||
|
| System | Responsibility |
|
||||||
|
|--------|---------------|
|
||||||
|
| `WallSystem` | Generates wall geometry with mitering and CSG cutouts for doors/windows |
|
||||||
|
| `SlabSystem` | Generates floor geometry from polygons |
|
||||||
|
| `CeilingSystem` | Generates ceiling geometry |
|
||||||
|
| `RoofSystem` | Generates roof geometry |
|
||||||
|
| `ItemSystem` | Positions items on walls, ceilings, or floors (slab elevation) |
|
||||||
|
|
||||||
|
**Viewer Systems (in `@pascal-app/viewer`):**
|
||||||
|
|
||||||
|
| System | Responsibility |
|
||||||
|
|--------|---------------|
|
||||||
|
| `LevelSystem` | Handles level visibility and vertical positioning (stacked/exploded/solo modes) |
|
||||||
|
| `ScanSystem` | Controls 3D scan visibility |
|
||||||
|
| `GuideSystem` | Controls guide image visibility |
|
||||||
|
|
||||||
|
**Processing Pattern:**
|
||||||
|
```typescript
|
||||||
|
useFrame(() => {
|
||||||
|
for (const id of dirtyNodes) {
|
||||||
|
const obj = sceneRegistry.nodes.get(id)
|
||||||
|
const node = useScene.getState().nodes[id]
|
||||||
|
|
||||||
|
// Update geometry, transforms, etc.
|
||||||
|
updateGeometry(obj, node)
|
||||||
|
|
||||||
|
dirtyNodes.delete(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dirty Nodes
|
||||||
|
|
||||||
|
When a node changes, it's marked as **dirty** in `useScene.getState().dirtyNodes`. Systems check this set each frame and only recompute geometry for dirty nodes.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Automatic: createNode, updateNode, deleteNode mark nodes dirty
|
||||||
|
useScene.getState().updateNode(wallId, { thickness: 0.2 })
|
||||||
|
// → wallId added to dirtyNodes
|
||||||
|
// → WallSystem regenerates geometry next frame
|
||||||
|
// → wallId removed from dirtyNodes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual marking:**
|
||||||
|
```typescript
|
||||||
|
useScene.getState().dirtyNodes.add(wallId)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Event Bus
|
||||||
|
|
||||||
|
Inter-component communication uses a typed event emitter (mitt):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Node events
|
||||||
|
emitter.on('wall:click', (event) => { ... })
|
||||||
|
emitter.on('item:enter', (event) => { ... })
|
||||||
|
emitter.on('zone:context-menu', (event) => { ... })
|
||||||
|
|
||||||
|
// Grid events (background)
|
||||||
|
emitter.on('grid:click', (event) => { ... })
|
||||||
|
|
||||||
|
// Event payload
|
||||||
|
NodeEvent {
|
||||||
|
node: AnyNode
|
||||||
|
position: [x, y, z]
|
||||||
|
localPosition: [x, y, z]
|
||||||
|
normal?: [x, y, z]
|
||||||
|
stopPropagation: () => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Spatial Grid Manager
|
||||||
|
|
||||||
|
Handles collision detection and placement validation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)
|
||||||
|
spatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)
|
||||||
|
spatialGridManager.getSlabElevationAt(levelId, x, z)
|
||||||
|
```
|
||||||
|
|
||||||
|
Used by item placement tools to validate positions and calculate slab elevations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Editor Architecture
|
||||||
|
|
||||||
|
The editor extends the viewer with:
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
Tools are activated via the toolbar and handle user input for specific operations:
|
||||||
|
|
||||||
|
- **SelectTool** - Selection and manipulation
|
||||||
|
- **WallTool** - Draw walls
|
||||||
|
- **ZoneTool** - Create zones
|
||||||
|
- **ItemTool** - Place furniture/fixtures
|
||||||
|
- **SlabTool** - Create floor slabs
|
||||||
|
|
||||||
|
### Selection Manager
|
||||||
|
|
||||||
|
The editor uses a custom selection manager with hierarchical navigation:
|
||||||
|
|
||||||
|
```
|
||||||
|
Site → Building → Level → Zone → Items
|
||||||
|
```
|
||||||
|
|
||||||
|
Each depth level has its own selection strategy for hover/click behavior.
|
||||||
|
|
||||||
|
### Editor-Specific Systems
|
||||||
|
|
||||||
|
- `ZoneSystem` - Controls zone visibility based on level mode
|
||||||
|
- Custom camera controls with node focusing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User Action (click, drag)
|
||||||
|
↓
|
||||||
|
Tool Handler
|
||||||
|
↓
|
||||||
|
useScene.createNode() / updateNode()
|
||||||
|
↓
|
||||||
|
Node added/updated in store
|
||||||
|
Node marked dirty
|
||||||
|
↓
|
||||||
|
React re-renders NodeRenderer
|
||||||
|
useRegistry() registers 3D object
|
||||||
|
↓
|
||||||
|
System detects dirty node (useFrame)
|
||||||
|
Updates geometry via sceneRegistry
|
||||||
|
Clears dirty flag
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **React 19** + **Next.js 16**
|
||||||
|
- **Three.js** (WebGPU renderer)
|
||||||
|
- **React Three Fiber** + **Drei**
|
||||||
|
- **Zustand** (state management)
|
||||||
|
- **Zod** (schema validation)
|
||||||
|
- **Zundo** (undo/redo)
|
||||||
|
- **three-bvh-csg** (Boolean geometry operations)
|
||||||
|
- **Turborepo** (monorepo management)
|
||||||
|
- **Bun** (package manager)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Run the development server from the **root directory** to enable hot reload for all packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Run development server (builds packages + starts editor with watch mode)
|
||||||
|
bun dev
|
||||||
|
|
||||||
|
# This will:
|
||||||
|
# 1. Build @pascal-app/core and @pascal-app/viewer
|
||||||
|
# 2. Start watching both packages for changes
|
||||||
|
# 3. Start the Next.js editor dev server
|
||||||
|
# Open http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Always run `bun dev` from the root directory to ensure the package watchers are running. This enables hot reload when you edit files in `packages/core/src/` or `packages/viewer/src/`.
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build all packages
|
||||||
turbo build
|
turbo build
|
||||||
|
|
||||||
# Without [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation), use your package manager
|
# Build specific package
|
||||||
npx turbo build
|
turbo build --filter=@pascal-app/core
|
||||||
yarn dlx turbo build
|
|
||||||
pnpm exec turbo build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can build a specific package by using a [filter](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters):
|
### Publishing Packages
|
||||||
|
|
||||||
```
|
```bash
|
||||||
# With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended)
|
# Build packages
|
||||||
turbo build --filter=docs
|
turbo build --filter=@pascal-app/core --filter=@pascal-app/viewer
|
||||||
|
|
||||||
# Without [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation), use your package manager
|
# Publish to npm
|
||||||
npx turbo build --filter=docs
|
npm publish --workspace=@pascal-app/core --access public
|
||||||
yarn exec turbo build --filter=docs
|
npm publish --workspace=@pascal-app/viewer --access public
|
||||||
pnpm exec turbo build --filter=docs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Develop
|
---
|
||||||
|
|
||||||
To develop all apps and packages, run the following command:
|
## Key Files
|
||||||
|
|
||||||
```
|
| Path | Description |
|
||||||
cd my-turborepo
|
|------|-------------|
|
||||||
|
| `packages/core/src/schema/` | Node type definitions (Zod schemas) |
|
||||||
# With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended)
|
| `packages/core/src/store/use-scene.ts` | Scene state store |
|
||||||
turbo dev
|
| `packages/core/src/hooks/scene-registry/` | 3D object registry |
|
||||||
|
| `packages/core/src/systems/` | Geometry generation systems |
|
||||||
# Without [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation), use your package manager
|
| `packages/viewer/src/components/renderers/` | Node renderers |
|
||||||
npx turbo dev
|
| `packages/viewer/src/components/viewer/` | Main Viewer component |
|
||||||
yarn exec turbo dev
|
| `apps/editor/components/tools/` | Editor tools |
|
||||||
pnpm exec turbo dev
|
| `apps/editor/store/` | Editor-specific state |
|
||||||
```
|
|
||||||
|
|
||||||
You can develop a specific package by using a [filter](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters):
|
|
||||||
|
|
||||||
```
|
|
||||||
# With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended)
|
|
||||||
turbo dev --filter=web
|
|
||||||
|
|
||||||
# Without [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation), use your package manager
|
|
||||||
npx turbo dev --filter=web
|
|
||||||
yarn exec turbo dev --filter=web
|
|
||||||
pnpm exec turbo dev --filter=web
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remote Caching
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
|
|
||||||
|
|
||||||
Turborepo can use a technique known as [Remote Caching](https://turborepo.dev/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
|
|
||||||
|
|
||||||
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd my-turborepo
|
|
||||||
|
|
||||||
# With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended)
|
|
||||||
turbo login
|
|
||||||
|
|
||||||
# Without [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation), use your package manager
|
|
||||||
npx turbo login
|
|
||||||
yarn exec turbo login
|
|
||||||
pnpm exec turbo login
|
|
||||||
```
|
|
||||||
|
|
||||||
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
|
|
||||||
|
|
||||||
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
|
|
||||||
|
|
||||||
```
|
|
||||||
# With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended)
|
|
||||||
turbo link
|
|
||||||
|
|
||||||
# Without [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation), use your package manager
|
|
||||||
npx turbo link
|
|
||||||
yarn exec turbo link
|
|
||||||
pnpm exec turbo link
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful Links
|
|
||||||
|
|
||||||
Learn more about the power of Turborepo:
|
|
||||||
|
|
||||||
- [Tasks](https://turborepo.dev/docs/crafting-your-repository/running-tasks)
|
|
||||||
- [Caching](https://turborepo.dev/docs/crafting-your-repository/caching)
|
|
||||||
- [Remote Caching](https://turborepo.dev/docs/core-concepts/remote-caching)
|
|
||||||
- [Filtering](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters)
|
|
||||||
- [Configuration Options](https://turborepo.dev/docs/reference/configuration)
|
|
||||||
- [CLI Usage](https://turborepo.dev/docs/reference/command-line-reference)
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import useEditor, { CatalogCategory } from "@/store/use-editor";
|
import useEditor, { CatalogCategory } from "@/store/use-editor";
|
||||||
import { CATALOG_ITEMS } from "./catalog-items";
|
import { CATALOG_ITEMS } from "./catalog-items";
|
||||||
import { AssetInput } from "@pascal-app/core";
|
import { AssetInput } from "@pascal-app/core";
|
||||||
|
import { resolveCdnUrl } from "@pascal-app/viewer";
|
||||||
|
|
||||||
export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
||||||
const selectedItem = useEditor((state) => state.selectedItem);
|
const selectedItem = useEditor((state) => state.selectedItem);
|
||||||
@@ -62,7 +63,7 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
|||||||
alt={item.name}
|
alt={item.name}
|
||||||
className="rounded-lg object-cover"
|
className="rounded-lg object-cover"
|
||||||
fill
|
fill
|
||||||
src={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-0.5 bottom-0.5 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
import type { NextConfig } from 'next'
|
||||||
const nextConfig = {
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
transpilePackages: ['three', '@pascal-app/viewer', '@pascal-app/core'],
|
transpilePackages: ['three', '@pascal-app/viewer', '@pascal-app/core'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,5 +17,9 @@
|
|||||||
"next.config.js",
|
"next.config.js",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../packages/core" },
|
||||||
|
{ "path": "../../packages/viewer" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 0,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "editor",
|
"name": "editor",
|
||||||
@@ -63,14 +62,16 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@pascal-app/core",
|
"name": "@pascal-app/core",
|
||||||
"version": "0.0.1",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dedent": "^1.7.1",
|
"dedent": "^1.7.1",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"three-bvh-csg": "^0.0.17",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
"zundo": "^2.3.0",
|
"zundo": "^2.3.0",
|
||||||
|
"zustand": "^5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
"@react-three/fiber": "^9",
|
"@react-three/fiber": "^9",
|
||||||
|
"react": "^18 || ^19",
|
||||||
"three": "^0.182",
|
"three": "^0.182",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -124,7 +126,10 @@
|
|||||||
},
|
},
|
||||||
"packages/viewer": {
|
"packages/viewer": {
|
||||||
"name": "@pascal-app/viewer",
|
"name": "@pascal-app/viewer",
|
||||||
"version": "0.0.1",
|
"version": "0.1.3",
|
||||||
|
"dependencies": {
|
||||||
|
"zustand": "^5",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
@@ -132,9 +137,10 @@
|
|||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@pascal-app/core": "*",
|
"@pascal-app/core": "^0.1.3",
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
"@react-three/fiber": "^9",
|
"@react-three/fiber": "^9",
|
||||||
|
"react": "^18 || ^19",
|
||||||
"three": "^0.182",
|
"three": "^0.182",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# @pascal-app/core
|
||||||
|
|
||||||
|
Core library for Pascal 3D building editor.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @pascal-app/core
|
||||||
|
```
|
||||||
|
|
||||||
|
## Peer Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react three @react-three/fiber @react-three/drei
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
- **Node Schemas** - Zod schemas for all building primitives (walls, slabs, items, etc.)
|
||||||
|
- **Scene State** - Zustand store with IndexedDB persistence and undo/redo
|
||||||
|
- **Systems** - Geometry generation for walls, floors, ceilings, roofs
|
||||||
|
- **Scene Registry** - Fast lookup from node IDs to Three.js objects
|
||||||
|
- **Spatial Grid** - Collision detection and placement validation
|
||||||
|
- **Event Bus** - Typed event emitter for inter-component communication
|
||||||
|
- **Asset Storage** - IndexedDB-based file storage for user-uploaded assets
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useScene, WallNode, ItemNode } from '@pascal-app/core'
|
||||||
|
|
||||||
|
// Create a wall
|
||||||
|
const wall = WallNode.parse({
|
||||||
|
points: [[0, 0], [5, 0]],
|
||||||
|
height: 3,
|
||||||
|
thickness: 0.2,
|
||||||
|
})
|
||||||
|
|
||||||
|
useScene.getState().createNode(wall, parentLevelId)
|
||||||
|
|
||||||
|
// Subscribe to scene changes
|
||||||
|
function MyComponent() {
|
||||||
|
const nodes = useScene((state) => state.nodes)
|
||||||
|
const walls = Object.values(nodes).filter(n => n.type === 'wall')
|
||||||
|
|
||||||
|
return <div>Total walls: {walls.length}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Types
|
||||||
|
|
||||||
|
- `SiteNode` - Root container
|
||||||
|
- `BuildingNode` - Building within a site
|
||||||
|
- `LevelNode` - Floor level
|
||||||
|
- `WallNode` - Vertical wall with optional openings
|
||||||
|
- `SlabNode` - Floor slab
|
||||||
|
- `CeilingNode` - Ceiling surface
|
||||||
|
- `RoofNode` - Roof geometry
|
||||||
|
- `ZoneNode` - Spatial zone/room
|
||||||
|
- `ItemNode` - Furniture, fixtures, appliances
|
||||||
|
- `ScanNode` - 3D scan reference
|
||||||
|
- `GuideNode` - 2D guide image reference
|
||||||
|
|
||||||
|
## Systems
|
||||||
|
|
||||||
|
Systems process dirty nodes each frame to update geometry:
|
||||||
|
|
||||||
|
- `WallSystem` - Wall geometry with mitering and CSG cutouts
|
||||||
|
- `SlabSystem` - Floor polygon generation
|
||||||
|
- `CeilingSystem` - Ceiling geometry
|
||||||
|
- `RoofSystem` - Roof generation
|
||||||
|
- `ItemSystem` - Item positioning on walls/ceilings/floors
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+41
-14
@@ -1,33 +1,60 @@
|
|||||||
{
|
{
|
||||||
"name": "@pascal-app/core",
|
"name": "@pascal-app/core",
|
||||||
"version": "0.0.1",
|
"version": "0.1.10",
|
||||||
"private": true,
|
"description": "Core library for Pascal 3D building editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./dist/index.js",
|
||||||
"types": "./src/index.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./src/index.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./src/index.ts"
|
"import": "./dist/index.js",
|
||||||
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build",
|
||||||
|
"dev": "tsc --build --watch",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
"@react-three/fiber": "^9",
|
"@react-three/fiber": "^9",
|
||||||
|
"react": "^18 || ^19",
|
||||||
"three": "^0.182"
|
"three": "^0.182"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dedent": "^1.7.1",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"three-bvh-csg": "^0.0.17",
|
||||||
|
"zod": "^4.3.5",
|
||||||
|
"zundo": "^2.3.0",
|
||||||
|
"zustand": "^5"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"@types/three": "^0.182.0"
|
"@types/three": "^0.182.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"keywords": [
|
||||||
"dedent": "^1.7.1",
|
"3d",
|
||||||
"idb-keyval": "^6.2.2",
|
"building",
|
||||||
"mitt": "^3.0.1",
|
"editor",
|
||||||
"nanoid": "^5.1.6",
|
"architecture",
|
||||||
"zod": "^4.3.5",
|
"webgpu",
|
||||||
"zundo": "^2.3.0"
|
"three.js"
|
||||||
}
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/your-username/pascal-editor.git",
|
||||||
|
"directory": "packages/core"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
"extends": "@repo/typescript-config/react-library.json",
|
"extends": "@repo/typescript-config/react-library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src"
|
"rootDir": "src",
|
||||||
|
"composite": true,
|
||||||
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": ["src", "../viewer/src/systems/level"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"noEmit": true
|
"noEmit": true,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# @pascal-app/viewer
|
||||||
|
|
||||||
|
3D viewer component for Pascal building editor.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @pascal-app/viewer @pascal-app/core
|
||||||
|
```
|
||||||
|
|
||||||
|
## Peer Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react three @react-three/fiber @react-three/drei
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
- **Viewer Component** - WebGPU-powered 3D viewer with camera controls
|
||||||
|
- **Node Renderers** - React Three Fiber components for all node types
|
||||||
|
- **Post-Processing** - SSGI (ambient occlusion + global illumination), TRAA (anti-aliasing), outline effects
|
||||||
|
- **Level System** - Level visibility and positioning (stacked/exploded/solo modes)
|
||||||
|
- **Wall Cutout System** - Dynamic wall hiding based on camera position
|
||||||
|
- **Asset URL Helpers** - CDN URL resolution for models and textures
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Viewer, useViewer } from '@pascal-app/viewer'
|
||||||
|
import { useScene } from '@pascal-app/core'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100vw', height: '100vh' }}>
|
||||||
|
<Viewer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Camera Controls
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Viewer } from '@pascal-app/viewer'
|
||||||
|
import { CameraControls } from '@react-three/drei'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Viewer selectionManager="custom">
|
||||||
|
<CameraControls />
|
||||||
|
</Viewer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Viewer State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
|
||||||
|
function ViewerControls() {
|
||||||
|
const levelMode = useViewer(s => s.levelMode)
|
||||||
|
const setLevelMode = useViewer(s => s.setLevelMode)
|
||||||
|
const wallMode = useViewer(s => s.wallMode)
|
||||||
|
const setWallMode = useViewer(s => s.setWallMode)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setLevelMode('stacked')}>Stacked</button>
|
||||||
|
<button onClick={() => setLevelMode('exploded')}>Exploded</button>
|
||||||
|
<button onClick={() => setWallMode('cutaway')}>Cutaway</button>
|
||||||
|
<button onClick={() => setWallMode('up')}>Full Height</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Asset CDN Helpers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { resolveCdnUrl, ASSETS_CDN_URL } from '@pascal-app/viewer'
|
||||||
|
|
||||||
|
// Resolves relative paths to CDN URLs
|
||||||
|
const url = resolveCdnUrl('/items/chair/model.glb')
|
||||||
|
// → 'https://pascal-cdn.wawasensei.dev/items/chair/model.glb'
|
||||||
|
|
||||||
|
// Handles external URLs and asset:// protocol
|
||||||
|
const externalUrl = resolveCdnUrl('https://example.com/model.glb')
|
||||||
|
// → 'https://example.com/model.glb' (unchanged)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **WebGPU Rendering** - Hardware-accelerated rendering via Three.js WebGPU
|
||||||
|
- **Post-Processing** - SSGI for realistic lighting, outline effects for selection
|
||||||
|
- **Level Modes** - Stacked, exploded, or solo level display
|
||||||
|
- **Wall Cutaway** - Automatic wall hiding for interior views
|
||||||
|
- **Camera Modes** - Perspective and orthographic projection
|
||||||
|
- **Scan/Guide Support** - 3D scans and 2D guide images
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -1,26 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "@pascal-app/viewer",
|
"name": "@pascal-app/viewer",
|
||||||
"version": "0.0.1",
|
"version": "0.1.10",
|
||||||
"private": true,
|
"description": "3D viewer component for Pascal building editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./dist/index.js",
|
||||||
"types": "./src/index.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./src/index.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./src/index.ts"
|
"import": "./dist/index.js",
|
||||||
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build",
|
||||||
|
"dev": "tsc --build --watch",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@pascal-app/core": "*",
|
"@pascal-app/core": "^0.1.4",
|
||||||
"@react-three/drei": "^10",
|
"@react-three/drei": "^10",
|
||||||
"@react-three/fiber": "^9",
|
"@react-three/fiber": "^9",
|
||||||
|
"react": "^18 || ^19",
|
||||||
"three": "^0.182"
|
"three": "^0.182"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zustand": "^5"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"@types/three": "^0.182.0"
|
"@types/three": "^0.182.0"
|
||||||
}
|
},
|
||||||
|
"keywords": [
|
||||||
|
"3d",
|
||||||
|
"building",
|
||||||
|
"editor",
|
||||||
|
"viewer",
|
||||||
|
"architecture",
|
||||||
|
"webgpu",
|
||||||
|
"three.js",
|
||||||
|
"react-three-fiber"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/your-username/pascal-editor.git",
|
||||||
|
"directory": "packages/viewer"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useGLTF } from '@react-three/drei/core/Gltf'
|
|||||||
import { Suspense, useEffect, useMemo, useRef } from 'react'
|
import { Suspense, useEffect, useMemo, useRef } from 'react'
|
||||||
import type { Group, Material, Mesh } from 'three'
|
import type { Group, Material, Mesh } from 'three'
|
||||||
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu'
|
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu'
|
||||||
|
import { resolveCdnUrl } from '../../../lib/asset-url'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
|
|
||||||
// Shared materials to avoid creating new instances for every mesh
|
// Shared materials to avoid creating new instances for every mesh
|
||||||
@@ -46,7 +47,7 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
||||||
const { scene, nodes } = useGLTF(node.asset.src)
|
const { scene, nodes } = useGLTF(resolveCdnUrl(node.asset.src) || '')
|
||||||
|
|
||||||
if (nodes.cutout) {
|
if (nodes.cutout) {
|
||||||
nodes.cutout.visible = false
|
nodes.cutout.visible = false
|
||||||
@@ -64,19 +65,19 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
if ((child as Mesh).isMesh) {
|
if ((child as Mesh).isMesh) {
|
||||||
const mesh = child as Mesh
|
const mesh = child as Mesh
|
||||||
if (mesh.name === 'cutout') {
|
if (mesh.name === 'cutout') {
|
||||||
child.visible = false;
|
child.visible = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasGlass = false;
|
let hasGlass = false
|
||||||
|
|
||||||
// Handle both single material and material array cases
|
// Handle both single material and material array cases
|
||||||
if (Array.isArray(mesh.material)) {
|
if (Array.isArray(mesh.material)) {
|
||||||
mesh.material = mesh.material.map((mat) => getMaterialForOriginal(mat))
|
mesh.material = mesh.material.map((mat) => getMaterialForOriginal(mat))
|
||||||
hasGlass = mesh.material.some(mat => mat.name === 'glass');
|
hasGlass = mesh.material.some((mat) => mat.name === 'glass')
|
||||||
} else {
|
} else {
|
||||||
mesh.material = getMaterialForOriginal(mesh.material)
|
mesh.material = getMaterialForOriginal(mesh.material)
|
||||||
hasGlass = mesh.material.name === 'glass';
|
hasGlass = mesh.material.name === 'glass'
|
||||||
}
|
}
|
||||||
mesh.castShadow = !hasGlass
|
mesh.castShadow = !hasGlass
|
||||||
mesh.receiveShadow = !hasGlass
|
mesh.receiveShadow = !hasGlass
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const ScanRenderer = ({ node }: { node: ScanNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ScanModel = ({ url, opacity }: { url: string; opacity: number }) => {
|
const ScanModel = ({ url, opacity }: { url: string; opacity: number }) => {
|
||||||
const { scene } = useGLTFKTX2(url)
|
const gltf = useGLTFKTX2(url) as any
|
||||||
|
const scene = gltf.scene
|
||||||
|
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
const normalizedOpacity = opacity / 100
|
const normalizedOpacity = opacity / 100
|
||||||
@@ -44,7 +45,7 @@ const ScanModel = ({ url, opacity }: { url: string; opacity: number }) => {
|
|||||||
material.needsUpdate = true
|
material.needsUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.traverse((child) => {
|
scene.traverse((child: any) => {
|
||||||
if ((child as Mesh).isMesh) {
|
if ((child as Mesh).isMesh) {
|
||||||
const mesh = child as Mesh
|
const mesh = child as Mesh
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.j
|
|||||||
const ktx2LoaderInstance = new KTX2Loader()
|
const ktx2LoaderInstance = new KTX2Loader()
|
||||||
ktx2LoaderInstance.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/')
|
ktx2LoaderInstance.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/')
|
||||||
|
|
||||||
const useGLTFKTX2 = (path: string) => {
|
const useGLTFKTX2 = (path: string): ReturnType<typeof useGLTF> => {
|
||||||
const gl = useThree((state) => state.gl)
|
const gl = useThree((state) => state.gl)
|
||||||
|
|
||||||
return useGLTF(path, true, true, (loader) => {
|
return useGLTF(path, true, true, (loader) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { default as Viewer } from './components/viewer'
|
export { default as Viewer } from './components/viewer'
|
||||||
export { useGridEvents } from './hooks/use-grid-events'
|
export { useGridEvents } from './hooks/use-grid-events'
|
||||||
export { default as useViewer } from './store/use-viewer'
|
export { default as useViewer } from './store/use-viewer'
|
||||||
|
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { loadAssetUrl } from '@pascal-app/core'
|
||||||
|
|
||||||
|
export const ASSETS_CDN_URL = 'https://editor.pascal.app'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an asset URL to the appropriate format:
|
||||||
|
* - If URL starts with http:// or https://, return as-is (external URL)
|
||||||
|
* - If URL starts with asset://, resolve from IndexedDB storage
|
||||||
|
* - If URL starts with /, prepend CDN URL (absolute path)
|
||||||
|
* - Otherwise, prepend CDN URL (relative path)
|
||||||
|
*/
|
||||||
|
export async function resolveAssetUrl(url: string | undefined | null): Promise<string | null> {
|
||||||
|
if (!url) return null
|
||||||
|
|
||||||
|
// External URL - use as-is
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexedDB asset - resolve from storage
|
||||||
|
if (url.startsWith('asset://')) {
|
||||||
|
return loadAssetUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute or relative path - prepend CDN URL
|
||||||
|
const normalizedPath = url.startsWith('/') ? url : `/${url}`
|
||||||
|
return `${ASSETS_CDN_URL}${normalizedPath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous version for URLs that don't need IndexedDB resolution
|
||||||
|
* Only use this if you're sure the URL is not an asset:// URL
|
||||||
|
*/
|
||||||
|
export function resolveCdnUrl(url: string | undefined | null): string | null {
|
||||||
|
if (!url) return null
|
||||||
|
|
||||||
|
// External URL - use as-is
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't use this for asset:// URLs - use resolveAssetUrl instead
|
||||||
|
if (url.startsWith('asset://')) {
|
||||||
|
console.warn('Use resolveAssetUrl() for asset:// URLs, not resolveCdnUrl()')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute or relative path - prepend CDN URL
|
||||||
|
const normalizedPath = url.startsWith('/') ? url : `/${url}`
|
||||||
|
return `${ASSETS_CDN_URL}${normalizedPath}`
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from "@pascal-app/core";
|
||||||
|
import type { Object3D } from "three";
|
||||||
|
type SelectionPath = {
|
||||||
|
buildingId: BuildingNode["id"] | null;
|
||||||
|
levelId: LevelNode["id"] | null;
|
||||||
|
zoneId: ZoneNode["id"] | null;
|
||||||
|
selectedIds: BaseNode["id"][];
|
||||||
|
};
|
||||||
|
type Outliner = {
|
||||||
|
selectedObjects: Object3D[];
|
||||||
|
hoveredObjects: Object3D[];
|
||||||
|
};
|
||||||
|
type ViewerState = {
|
||||||
|
selection: SelectionPath;
|
||||||
|
hoveredId: AnyNode['id'] | ZoneNode['id'] | null;
|
||||||
|
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void;
|
||||||
|
cameraMode: 'perspective' | 'orthographic';
|
||||||
|
setCameraMode: (mode: 'perspective' | 'orthographic') => void;
|
||||||
|
levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
|
||||||
|
setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
|
||||||
|
wallMode: 'up' | 'cutaway' | 'down';
|
||||||
|
setWallMode: (mode: 'up' | 'cutaway' | 'down') => void;
|
||||||
|
showScans: boolean;
|
||||||
|
setShowScans: (show: boolean) => void;
|
||||||
|
showGuides: boolean;
|
||||||
|
setShowGuides: (show: boolean) => void;
|
||||||
|
setSelection: (updates: Partial<SelectionPath>) => void;
|
||||||
|
resetSelection: () => void;
|
||||||
|
outliner: Outliner;
|
||||||
|
exportScene: (() => Promise<void>) | null;
|
||||||
|
setExportScene: (fn: (() => Promise<void>) | null) => void;
|
||||||
|
};
|
||||||
|
declare const useViewer: import("zustand").UseBoundStore<import("zustand").StoreApi<ViewerState>>;
|
||||||
|
export default useViewer;
|
||||||
|
//# sourceMappingURL=use-viewer.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["use-viewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;CAC/B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,YAAY,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAEjE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAA;IAC1C,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,cAAc,KAAK,IAAI,CAAA;IAE7D,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;IACrD,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAExE,QAAQ,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,KAAK,IAAI,CAAA;IAEtD,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAErC,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAGtC,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IACvD,cAAc,EAAE,MAAM,IAAI,CAAA;IAE1B,QAAQ,EAAE,QAAQ,CAAA;IAGlB,WAAW,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IACzC,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;CAC3D,CAAA;AAED,QAAA,MAAM,SAAS,0EAqDZ,CAAC;AAEJ,eAAe,SAAS,CAAC"}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export declare const LevelSystem: () => null;
|
||||||
|
//# sourceMappingURL=level-system.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["level-system.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,YAkBvB,CAAA"}
|
||||||
@@ -2,8 +2,13 @@
|
|||||||
"extends": "@repo/typescript-config/react-library.json",
|
"extends": "@repo/typescript-config/react-library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src"
|
"rootDir": "src",
|
||||||
|
"composite": true,
|
||||||
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../core" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||||
"outputs": [".next/**", "!.next/cache/**"]
|
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": ["^lint"]
|
"dependsOn": ["^lint"]
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependsOn": ["^check-types"]
|
"dependsOn": ["^check-types"]
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user