# Pascal Editor Roof Geometry Generation with Segments: A Technical Deep Dive

> Explore how Pascal Editor generates roof geometry using independent RoofSegmentNode objects and Constructive Solid Geometry CSG operations for efficient rendering.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: deep-dive
- Published: 2026-03-25

---

**The Pascal Editor generates roof geometry by representing each roof module as independent `RoofSegmentNode` objects in a scene graph, processing them through Constructive Solid Geometry (CSG) operations to create hollow walls and shingle volumes, and ultimately merging all segments into a single optimized `THREE.Mesh` for rendering.**

Pascal Editor is an open-source 3D architectural design tool that handles complex roof structures through a hybrid approach—maintaining editable granularity during design time while delivering batched, high-performance rendering at runtime. The system relies on a specialized **RoofSystem** ([`packages/core/src/systems/roof/roof-system.tsx`](https://github.com/pascalorg/editor/blob/main/packages/core/src/systems/roof/roof-system.tsx)) that bridges the declarative node schema with imperative Three.js geometry operations, ensuring that roof segments remain interactive while the final merged mesh minimizes draw calls.

## Data Model and Scene Graph Architecture

The roof geometry pipeline begins with a strict node hierarchy defined in the core schema. A **RoofNode** serves as a logical container that groups one or more **RoofSegmentNode** instances, each representing a distinct architectural module (gable, hip, shed, etc.) with complete dimensional parameters.

The critical files defining this structure are:

- **[`packages/core/src/schema/nodes/roof.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/nodes/roof.ts)** – Defines `RoofNode`, which holds `position`, `rotation`, and an array of `children: RoofSegmentNode[]`. This node has no geometry of its own; it functions purely as a grouping mechanism.
- **[`packages/core/src/schema/nodes/roof-segment.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/schema/nodes/roof-segment.ts)** – Defines `RoofSegmentNode`, which stores geometric parameters including `roofType`, `width`, `depth`, `wallHeight`, `roofHeight`, `wallThickness`, `overhang`, `shingleThickness`, and `deckThickness`.
- **[`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts)** – The Zustand-based global state management that tracks `nodes`, `dirtyNodes`, and `rootNodeIds`. It exposes `markDirty(id)` to invalidate geometry and `clearDirty(id)` to signal completion.
- **[`packages/core/src/hooks/scene-registry/scene-registry.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/hooks/scene-registry/scene-registry.ts)** – Maps node IDs to live Three.js objects (`THREE.Mesh` or `THREE.Group`), allowing the system to retrieve visual objects for both individual segments and merged roofs.

This separation allows the editor to manipulate high-level parameters (like wall height or roof pitch) while the system recomputes the underlying volumetric geometry automatically.

## The Geometry Generation Pipeline

The `RoofSystem` executes a four-stage pipeline each frame: dirty detection, segment geometry generation, CSG Boolean operations, and final mesh merging with material remapping.

### Dirty Detection and Segment Updates

When a user modifies a roof segment—changing `roofType` from `'gable'` to `'hip'` or adjusting `width`—the editor calls `useScene.getState().markDirty(segmentId)`. The `RoofSystem` reads the `dirtyNodes` Set each frame and processes visible segments immediately while deferring hidden ones.

If `mesh.parent?.visible !== false`, the system triggers `updateRoofSegmentGeometry`, which delegates to `generateRoofSegmentGeometry` to rebuild the segment’s individual mesh before the merge step occurs.

### CSG Brush Construction

For each dirty segment, the system constructs **four CSG brushes** representing the architectural components: outer walls, inner cavity, deck slab, and shingle slab. The process flows through these specific functions:

1. **`getModuleFaces`** – Computes the planar faces for the selected `roofType` (gable, hip, etc.) based on the segment’s width, depth, and height parameters.
2. **`createGeometryFromFaces`** – Converts the calculated faces into a `THREE.BufferGeometry`.
3. **`getRoofSegmentBrushes`** – Transforms the geometry into `Brush` objects (using `three-bvh-csg`) and performs **subtraction** operations to carve the hollow cavity from the solid walls, and **addition** operations to combine the deck and shingle volumes.

The result is a hollow wall structure with separate shingle and deck volumes, ready for world-space transformation.

### Mesh Merging and Material Mapping

After all dirty segments are processed, the parent roof enters the `pendingRoofUpdates` queue. The merging algorithm:

1. Retrieves every child segment and transforms each brush into world space using `_matrix.compose(position, rotation, scale)`.
2. **Cumulatively adds** brushes (`ADD` operation) to produce four aggregated volumes: `totalShinSlab`, `totalDeckSlab`, `totalWall`, and `totalInner`.
3. Performs CSG **subtractions** to carve the inner cavity from walls and trim deck/shingles against the interior volume.
4. Merges the results into a single `THREE.Mesh` named **`merged-roof`**.
5. Remaps material indices via `mapRoofGroupMaterialIndex` to ensure the four material slots (`roofMaterials[0-3]`) correspond correctly to walls, deck, shingle, and interior faces, regardless of the arbitrary group ordering created by the CSG library.

Dummy placeholder materials are used during CSG operations to satisfy the library requirements, then replaced with the actual material array after geometry finalization.

## Performance Optimization Strategies

The roof geometry generation implements aggressive throttling to maintain UI responsiveness. The system processes **a maximum of one roof and three segments per frame**, preventing frame drops during complex Boolean operations on large structures.

During editing mode, individual segment meshes remain visible and selectable, allowing precise manipulation. When the user exits edit mode, the `RoofEditSystem` marks the parent roof dirty, triggering the final merge. The editor then **hides individual segment meshes** and displays only the `merged-roof` mesh, reducing draw calls from *N* segments to a single draw call.

For segments that exist in the scene graph but are currently occluded or off-screen, the system substitutes a cheap `BoxGeometry` placeholder (immediately converted to an empty `BufferGeometry` with BVH) to maintain the spatial tree validity without executing expensive CSG calculations.

## Implementation Examples

### Creating a Multi-Segment Roof

The following pattern demonstrates how to programmatically create a roof container with two distinct segments using the core store:

```typescript
import { useScene } from '@pascal-app/core'
import { RoofNode, RoofSegmentNode } from '@pascal-app/core/schema'

// Create the roof container
const roofId = useScene.getState().createNode({
  type: 'roof',
  position: [0, 0, 0],
  rotation: 0,
})

// Create a gable segment
const segA = useScene.getState().createNode({
  type: 'roof-segment',
  roofType: 'gable',
  width: 8,
  depth: 6,
  wallHeight: 3,
  roofHeight: 2.5,
  wallThickness: 0.2,
  overhang: 0.3,
  shingleThickness: 0.02,
  deckThickness: 0.1,
  parentId: roofId,
})

// Create a shed segment
const segB = useScene.getState().createNode({
  type: 'roof-segment',
  roofType: 'shed',
  width: 8,
  depth: 6,
  wallHeight: 3,
  roofHeight: 2,
  wallThickness: 0.2,
  overhang: 0.3,
  shingleThickness: 0.02,
  deckThickness: 0.1,
  parentId: roofId,
})

// Trigger geometry rebuild
useScene.getState().markDirty(roofId)

```

### Updating Segment Geometry

To force a refresh after changing segment properties:

```typescript
// Modify the roof type
useScene.getState().updateNode(segA, (node) => ({
  ...node,
  roofType: 'hip',
}))

// Invalidate the segment mesh
useScene.getState().markDirty(segA)

```

### Accessing the Merged Mesh in the Viewer

The viewer package consumes the generated geometry through the scene registry:

```tsx
import { useRegistry } from '@pascal-app/viewer'

function RoofDebug({ roofId }: { roofId: string }) {
  const roofGroup = useRegistry(roofId) as THREE.Group | undefined
  const merged = roofGroup?.getObjectByName('merged-roof') as THREE.Mesh | undefined

  useEffect(() => {
    if (merged) {
      console.log('Merged roof geometry:', merged.geometry)
    }
  }, [merged])

  return null
}

```

## Summary

- **Pascal Editor** uses a two-tier node system: `RoofNode` containers and `RoofSegmentNode` geometry modules defined in `packages/core/src/schema/nodes/`.
- **CSG operations** in [`packages/core/src/systems/roof/roof-system.tsx`](https://github.com/pascalorg/editor/blob/main/packages/core/src/systems/roof/roof-system.tsx) generate hollow walls and shingle volumes using `three-bvh-csg` brushes.
- **Dirty detection** via `markDirty()` in the Zustand store ([`use-scene.ts`](https://github.com/pascalorg/editor/blob/main/use-scene.ts)) ensures geometry regenerates only for changed nodes.
- **Merged rendering** combines all segments into a single `merged-roof` mesh, reducing draw calls while preserving editability through individual segment meshes.
- **Throttled processing** limits CSG work to one roof and three segments per frame to maintain 60 FPS interaction.

## Frequently Asked Questions

### How does Pascal Editor handle different roof types like gable and hip roofs?

The editor stores the roof type in the `roofType` property of each `RoofSegmentNode`. When generating geometry, the `getModuleFaces` function computes the specific planar face configuration for that type—triangular faces for gables, trapezoidal faces for hips—then extrudes them into 3D volumes using CSG operations. All roof types use the same four-brush pipeline (walls, cavity, deck, shingle) but with different initial face geometries.

### What is the performance cost of editing large roofs with many segments?

The system throttles CSG calculations to **one roof and three segments per frame**, preventing UI freezing during complex operations. Additionally, the editor only renders individual segment meshes during active editing; it switches to the single merged mesh (`merged-roof`) when the user is not interacting with the roof, reducing GPU draw calls from *N* to 1.

### Why does the roof system use CSG Boolean operations instead of simple extrusion?

CSG operations are required to create **architecturally accurate hollow walls** with consistent thickness. The system subtracts the inner cavity from the outer wall volume and performs intersection operations to ensure the deck and shingles only exist where they structurally overlap with the wall volume. Simple extrusion would create solid blocks rather than enclosed architectural volumes with interior cavities.

### How are materials assigned to different parts of the merged roof?

After CSG operations complete, the `mapRoofGroupMaterialIndex` function rewrites the `materialIndex` property of each geometry group. The system maps the arbitrary groups created by the CSG library into four consistent material slots: index 0 for walls, 1 for deck, 2 for shingle, and 3 for interior faces. This mapping ensures the viewer’s material array (`roofMaterials[0-3]`) applies textures correctly regardless of the Boolean operation order.