Wassim SAMADandClaude Opus 4.7 969b154b08 Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C
Depth-first session: drive registered kinds through Stage B (pure
def.geometry, drop system re-export) and Stage C (def.floorplan,
short-circuit legacy inline rendering in floorplan-panel.tsx).

spawn → C
 - buildSpawnFloorplan wired on definition (was written but deferred
   to avoid double-render).
 - floorplan-panel.tsx's floorplanSpawnEntries useMemo short-circuits
   to [] when nodeRegistry.has('spawn').

fence → B
 - generateFenceGeometry exported from viewer; buildFenceGeometry
   wraps it in a Group+Mesh with DEFAULT_STAIR_MATERIAL.
 - def.geometry set; renderer + system fields dropped.
 - Deleted nodes/src/fence/{renderer.tsx,system.tsx}.

fence → C
 - buildFenceFloorplan: polyline along centerline (sampled for curved
   fences via sampleWallCenterline from core). Stroke width = node.thickness.
 - floorplan-panel.tsx's floorplanFenceEntries short-circuits.

slab → B
 - generateSlabGeometry exported from viewer; buildSlabGeometry wraps
   it in a Group+Mesh + cached material (preset / custom / default
   pattern preserved from legacy renderer).
 - def.geometry set; renderer + system fields dropped.
 - Deleted nodes/src/slab/{renderer.tsx,system.tsx}.

slab → C
 - buildSlabFloorplan: SVG path with outer polygon + hole subpaths
   (uses getRenderableSlabPolygon from core for wall-clipping parity).
 - floorplan-panel.tsx's slabPolygons short-circuits.

ceiling → B INTENTIONALLY SKIPPED
 - Ceiling renderer renders React children (hosted items) + uses TSL
   shader materials + named meshes that other systems poke
   (getObjectByName('ceiling-grid')). Pure def.geometry can't preserve
   that. Ceiling keeps def.renderer (the custom escape hatch) — same
   pattern item uses. Documented in ceiling/definition.ts.

ceiling → C
 - buildCeilingFloorplan: dashed-outline path with hole subpaths
   (visually distinct from slab since ceilings are above).
 - floorplan-panel.tsx's ceilingPolygons short-circuits.

Per-kind progress after this session:
 - shelf: B  C  (Stage E since brand-new)
 - spawn: A  C 
 - wall: A  (B blocked on ctx.levelData design)
 - fence: A  B  C 
 - slab: A  B  C 
 - ceiling: A  C  (B intentionally not applicable)
 - door / window / item: A  (B+C pending in future sessions)

Known test issue: `bun test` in packages/nodes fails to load
`three-bvh-csg` through the viewer's transitive imports (UMD/ESM
mismatch in Bun's test runner). The Next.js editor build works fine
because it bundles differently. Fix requires either dynamic imports
(breaks sync def.geometry contract) or test env config — deferred.
Other tests (schema, geometry, parity) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:14:22 -04:00
2026-01-21 10:11:30 +09:00
2026-03-13 09:07:46 +01:00
2026-04-18 16:25:03 +02:00
2026-01-14 07:26:59 +09:00
2026-03-10 09:40:07 +01:00
2026-02-11 06:35:56 +09:00
2026-05-09 02:25:25 +00:00

Pascal Editor

A 3D building editor built with React Three Fiber and WebGPU.

MIT License npm @pascal-app/core npm @pascal-app/viewer Discord X (Twitter)

https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b

Repository Architecture

This is a Turborepo monorepo with three main packages:

editor-v2/
├── apps/
│   └── editor/          # Next.js application
├── packages/
│   ├── core/            # Schema definitions, state management, systems
│   └── viewer/          # 3D rendering components

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:

// 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:

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:

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:

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:

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):

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:

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.

// 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:

useScene.getState().dirtyNodes.add(wallId)

Event Bus

Inter-component communication uses a typed event emitter (mitt):

// 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:

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:

# 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

# Build all packages
turbo build

# Build specific package
turbo build --filter=@pascal-app/core

Publishing Packages

# Build packages
turbo build --filter=@pascal-app/core --filter=@pascal-app/viewer

# Publish to npm
npm publish --workspace=@pascal-app/core --access public
npm publish --workspace=@pascal-app/viewer --access public

Key Files

Path Description
packages/core/src/schema/ Node type definitions (Zod schemas)
packages/core/src/store/use-scene.ts Scene state store
packages/core/src/hooks/scene-registry/ 3D object registry
packages/core/src/systems/ Geometry generation systems
packages/viewer/src/components/renderers/ Node renderers
packages/viewer/src/components/viewer/ Main Viewer component
apps/editor/components/tools/ Editor tools
apps/editor/store/ Editor-specific state

Contributors

Aymeric Rabot Wassim Samad Sudhir


pascalorg/editor | Trendshift

S
Description
Клон проекта Pascal Editor
Readme MIT
119 MiB
Languages
TypeScript 99.7%
CSS 0.1%
Shell 0.1%