{"id":48305478,"url":"https://github.com/pascalorg/editor","last_synced_at":"2026-04-14T07:00:28.203Z","repository":{"id":320434748,"uuid":"1077819522","full_name":"pascalorg/editor","owner":"pascalorg","description":"Create and share 3D architectural projects.","archived":false,"fork":false,"pushed_at":"2026-04-08T01:18:38.000Z","size":74893,"stargazers_count":9676,"open_issues_count":18,"forks_count":1244,"subscribers_count":61,"default_branch":"main","last_synced_at":"2026-04-08T01:32:10.030Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://editor.pascal.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pascalorg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-16T19:35:06.000Z","updated_at":"2026-04-08T01:18:43.000Z","dependencies_parsed_at":"2025-10-23T20:38:48.487Z","dependency_job_id":"536cdb41-9d3d-49f7-b908-4dea450e66d1","html_url":"https://github.com/pascalorg/editor","commit_stats":null,"previous_names":["pascalorg/editor"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/pascalorg/editor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pascalorg%2Feditor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pascalorg%2Feditor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pascalorg%2Feditor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pascalorg%2Feditor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pascalorg","download_url":"https://codeload.github.com/pascalorg/editor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pascalorg%2Feditor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31782872,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T02:24:21.117Z","status":"ssl_error","status_checked_at":"2026-04-14T02:24:20.627Z","response_time":153,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-05T00:00:38.368Z","updated_at":"2026-04-14T07:00:28.184Z","avatar_url":"https://github.com/pascalorg.png","language":"TypeScript","readme":"# Pascal Editor\n\nA 3D building editor built with React Three Fiber and WebGPU.\n\n[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![npm @pascal-app/core](https://img.shields.io/npm/v/@pascal-app/core?label=%40pascal-app%2Fcore)](https://www.npmjs.com/package/@pascal-app/core)\n[![npm @pascal-app/viewer](https://img.shields.io/npm/v/@pascal-app/viewer?label=%40pascal-app%2Fviewer)](https://www.npmjs.com/package/@pascal-app/viewer)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord\u0026logoColor=white)](https://discord.gg/SaBRA9t2)\n[![X (Twitter)](https://img.shields.io/badge/follow-%40pascal__app-black?logo=x\u0026logoColor=white)](https://x.com/pascal_app)\n\nhttps://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b\n\n\n\n## Repository Architecture\n\nThis is a Turborepo monorepo with three main packages:\n\n```\neditor-v2/\n├── apps/\n│   └── editor/          # Next.js application\n├── packages/\n│   ├── core/            # Schema definitions, state management, systems\n│   └── viewer/          # 3D rendering components\n```\n\n### Separation of Concerns\n\n| Package | Responsibility |\n|---------|---------------|\n| **@pascal-app/core** | Node schemas, scene state (Zustand), systems (geometry generation), spatial queries, event bus |\n| **@pascal-app/viewer** | 3D rendering via React Three Fiber, default camera/controls, post-processing |\n| **apps/editor** | UI components, tools, custom behaviors, editor-specific systems |\n\nThe **viewer** renders the scene with sensible defaults. The **editor** extends it with interactive tools, selection management, and editing capabilities.\n\n### Stores\n\nEach package has its own Zustand store for managing state:\n\n| Store | Package | Responsibility |\n|-------|---------|----------------|\n| `useScene` | `@pascal-app/core` | Scene data: nodes, root IDs, dirty nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo. |\n| `useViewer` | `@pascal-app/viewer` | Viewer state: current selection (building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode. |\n| `useEditor` | `apps/editor` | Editor state: active tool, structure layer visibility, panel states, editor-specific preferences. |\n\n**Access patterns:**\n\n```typescript\n// Subscribe to state changes (React component)\nconst nodes = useScene((state) =\u003e state.nodes)\nconst levelId = useViewer((state) =\u003e state.selection.levelId)\nconst activeTool = useEditor((state) =\u003e state.tool)\n\n// Access state outside React (callbacks, systems)\nconst node = useScene.getState().nodes[id]\nuseViewer.getState().setSelection({ levelId: 'level_123' })\n```\n\n---\n\n## Core Concepts\n\n### Nodes\n\nNodes are the data primitives that describe the 3D scene. All nodes extend `BaseNode`:\n\n```typescript\nBaseNode {\n  id: string              // Auto-generated with type prefix (e.g., \"wall_abc123\")\n  type: string            // Discriminator for type-safe handling\n  parentId: string | null // Parent node reference\n  visible: boolean\n  camera?: Camera         // Optional saved camera position\n  metadata?: JSON         // Arbitrary metadata (e.g., { isTransient: true })\n}\n```\n\n**Node Hierarchy:**\n\n```\nSite\n└── Building\n    └── Level\n        ├── Wall → Item (doors, windows)\n        ├── Slab\n        ├── Ceiling → Item (lights)\n        ├── Roof\n        ├── Zone\n        ├── Scan (3D reference)\n        └── Guide (2D reference)\n```\n\nNodes are stored in a **flat dictionary** (`Record\u003cid, Node\u003e`), not a nested tree. Parent-child relationships are defined via `parentId` and `children` arrays.\n\n---\n\n### Scene State (Zustand Store)\n\nThe scene is managed by a Zustand store in `@pascal-app/core`:\n\n```typescript\nuseScene.getState() = {\n  nodes: Record\u003cid, AnyNode\u003e,  // All nodes\n  rootNodeIds: string[],       // Top-level nodes (sites)\n  dirtyNodes: Set\u003cstring\u003e,     // Nodes pending system updates\n\n  createNode(node, parentId),\n  updateNode(id, updates),\n  deleteNode(id),\n}\n```\n\n**Middleware:**\n- **Persist** - Saves to IndexedDB (excludes transient nodes)\n- **Temporal** (Zundo) - Undo/redo with 50-step history\n\n---\n\n### Scene Registry\n\nThe registry maps node IDs to their Three.js objects for fast lookup:\n\n```typescript\nsceneRegistry = {\n  nodes: Map\u003cid, Object3D\u003e,    // ID → 3D object\n  byType: {\n    wall: Set\u003cid\u003e,\n    item: Set\u003cid\u003e,\n    zone: Set\u003cid\u003e,\n    // ...\n  }\n}\n```\n\nRenderers register their refs using the `useRegistry` hook:\n\n```tsx\nconst ref = useRef\u003cMesh\u003e(null!)\nuseRegistry(node.id, 'wall', ref)\n```\n\nThis allows systems to access 3D objects directly without traversing the scene graph.\n\n---\n\n### Node Renderers\n\nRenderers are React components that create Three.js objects for each node type:\n\n```\nSceneRenderer\n└── NodeRenderer (dispatches by type)\n    ├── BuildingRenderer\n    ├── LevelRenderer\n    ├── WallRenderer\n    ├── SlabRenderer\n    ├── ZoneRenderer\n    ├── ItemRenderer\n    └── ...\n```\n\n**Pattern:**\n1. Renderer creates a placeholder mesh/group\n2. Registers it with `useRegistry`\n3. Systems update geometry based on node data\n\nExample (simplified):\n```tsx\nconst WallRenderer = ({ node }) =\u003e {\n  const ref = useRef\u003cMesh\u003e(null!)\n  useRegistry(node.id, 'wall', ref)\n\n  return (\n    \u003cmesh ref={ref}\u003e\n      \u003cboxGeometry args={[0, 0, 0]} /\u003e  {/* Replaced by WallSystem */}\n      \u003cmeshStandardMaterial /\u003e\n      {node.children.map(id =\u003e \u003cNodeRenderer key={id} nodeId={id} /\u003e)}\n    \u003c/mesh\u003e\n  )\n}\n```\n\n---\n\n### Systems\n\nSystems are React components that run in the render loop (`useFrame`) to update geometry and transforms. They process **dirty nodes** marked by the store.\n\n**Core Systems (in `@pascal-app/core`):**\n\n| System | Responsibility |\n|--------|---------------|\n| `WallSystem` | Generates wall geometry with mitering and CSG cutouts for doors/windows |\n| `SlabSystem` | Generates floor geometry from polygons |\n| `CeilingSystem` | Generates ceiling geometry |\n| `RoofSystem` | Generates roof geometry |\n| `ItemSystem` | Positions items on walls, ceilings, or floors (slab elevation) |\n\n**Viewer Systems (in `@pascal-app/viewer`):**\n\n| System | Responsibility |\n|--------|---------------|\n| `LevelSystem` | Handles level visibility and vertical positioning (stacked/exploded/solo modes) |\n| `ScanSystem` | Controls 3D scan visibility |\n| `GuideSystem` | Controls guide image visibility |\n\n**Processing Pattern:**\n```typescript\nuseFrame(() =\u003e {\n  for (const id of dirtyNodes) {\n    const obj = sceneRegistry.nodes.get(id)\n    const node = useScene.getState().nodes[id]\n\n    // Update geometry, transforms, etc.\n    updateGeometry(obj, node)\n\n    dirtyNodes.delete(id)\n  }\n})\n```\n\n---\n\n### Dirty Nodes\n\nWhen 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.\n\n```typescript\n// Automatic: createNode, updateNode, deleteNode mark nodes dirty\nuseScene.getState().updateNode(wallId, { thickness: 0.2 })\n// → wallId added to dirtyNodes\n// → WallSystem regenerates geometry next frame\n// → wallId removed from dirtyNodes\n```\n\n**Manual marking:**\n```typescript\nuseScene.getState().dirtyNodes.add(wallId)\n```\n\n---\n\n### Event Bus\n\nInter-component communication uses a typed event emitter (mitt):\n\n```typescript\n// Node events\nemitter.on('wall:click', (event) =\u003e { ... })\nemitter.on('item:enter', (event) =\u003e { ... })\nemitter.on('zone:context-menu', (event) =\u003e { ... })\n\n// Grid events (background)\nemitter.on('grid:click', (event) =\u003e { ... })\n\n// Event payload\nNodeEvent {\n  node: AnyNode\n  position: [x, y, z]\n  localPosition: [x, y, z]\n  normal?: [x, y, z]\n  stopPropagation: () =\u003e void\n}\n```\n\n---\n\n### Spatial Grid Manager\n\nHandles collision detection and placement validation:\n\n```typescript\nspatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)\nspatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)\nspatialGridManager.getSlabElevationAt(levelId, x, z)\n```\n\nUsed by item placement tools to validate positions and calculate slab elevations.\n\n---\n\n## Editor Architecture\n\nThe editor extends the viewer with:\n\n### Tools\n\nTools are activated via the toolbar and handle user input for specific operations:\n\n- **SelectTool** - Selection and manipulation\n- **WallTool** - Draw walls\n- **ZoneTool** - Create zones\n- **ItemTool** - Place furniture/fixtures\n- **SlabTool** - Create floor slabs\n\n### Selection Manager\n\nThe editor uses a custom selection manager with hierarchical navigation:\n\n```\nSite → Building → Level → Zone → Items\n```\n\nEach depth level has its own selection strategy for hover/click behavior.\n\n### Editor-Specific Systems\n\n- `ZoneSystem` - Controls zone visibility based on level mode\n- Custom camera controls with node focusing\n\n---\n\n## Data Flow\n\n```\nUser Action (click, drag)\n       ↓\nTool Handler\n       ↓\nuseScene.createNode() / updateNode()\n       ↓\nNode added/updated in store\nNode marked dirty\n       ↓\nReact re-renders NodeRenderer\nuseRegistry() registers 3D object\n       ↓\nSystem detects dirty node (useFrame)\nUpdates geometry via sceneRegistry\nClears dirty flag\n```\n\n---\n\n## Technology Stack\n\n- **React 19** + **Next.js 16**\n- **Three.js** (WebGPU renderer)\n- **React Three Fiber** + **Drei**\n- **Zustand** (state management)\n- **Zod** (schema validation)\n- **Zundo** (undo/redo)\n- **three-bvh-csg** (Boolean geometry operations)\n- **Turborepo** (monorepo management)\n- **Bun** (package manager)\n\n---\n\n## Getting Started\n\n### Development\n\nRun the development server from the **root directory** to enable hot reload for all packages:\n\n```bash\n# Install dependencies\nbun install\n\n# Run development server (builds packages + starts editor with watch mode)\nbun dev\n\n# This will:\n# 1. Build @pascal-app/core and @pascal-app/viewer\n# 2. Start watching both packages for changes\n# 3. Start the Next.js editor dev server\n# Open http://localhost:3000\n```\n\n**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/`.\n\n### Building for Production\n\n```bash\n# Build all packages\nturbo build\n\n# Build specific package\nturbo build --filter=@pascal-app/core\n```\n\n### Publishing Packages\n\n```bash\n# Build packages\nturbo build --filter=@pascal-app/core --filter=@pascal-app/viewer\n\n# Publish to npm\nnpm publish --workspace=@pascal-app/core --access public\nnpm publish --workspace=@pascal-app/viewer --access public\n```\n\n---\n\n## Key Files\n\n| Path | Description |\n|------|-------------|\n| `packages/core/src/schema/` | Node type definitions (Zod schemas) |\n| `packages/core/src/store/use-scene.ts` | Scene state store |\n| `packages/core/src/hooks/scene-registry/` | 3D object registry |\n| `packages/core/src/systems/` | Geometry generation systems |\n| `packages/viewer/src/components/renderers/` | Node renderers |\n| `packages/viewer/src/components/viewer/` | Main Viewer component |\n| `apps/editor/components/tools/` | Editor tools |\n| `apps/editor/store/` | Editor-specific state |\n\n---\n\n## Contributors\n\n\u003ca href=\"https://github.com/Aymericr\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/4444492?v=4\" width=\"60\" height=\"60\" alt=\"Aymeric Rabot\" style=\"border-radius:50%\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/wass08\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/6551176?v=4\" width=\"60\" height=\"60\" alt=\"Wassim Samad\" style=\"border-radius:50%\"\u003e\u003c/a\u003e\n\n---\n\n\u003ca href=\"https://trendshift.io/repositories/23831\" target=\"_blank\"\u003e\u003cimg src=\"https://trendshift.io/api/badge/repositories/23831\" alt=\"pascalorg/editor | Trendshift\" width=\"250\" height=\"55\"/\u003e\u003c/a\u003e\n","funding_links":[],"categories":["TypeScript","🎨 Design \u0026 Creative","others"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpascalorg%2Feditor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpascalorg%2Feditor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpascalorg%2Feditor/lists"}