# Pascal Editor Wall System Geometry Generation: Mitering and CSG Pipeline

> Master Pascal Editor wall system geometry generation including mitering and CSG pipeline for accurate architectural models. Integrate doors and windows seamlessly.

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

---

**The Pascal Editor wall system generates real-time architectural geometry by detecting dirty wall nodes, computing mitered junction offsets, and applying CSG subtraction for door/window cut-outs using three-bvh-csg.**

The `pascalorg/editor` repository implements a deterministic, real-time wall geometry pipeline in the `@pascal-app/core` package. This system transforms declarative wall nodes into render-ready Three.js meshes through a three-stage process involving dirty-node tracking, precise miter calculations, and Boolean operations.

## Dirty-Node Detection and Level Grouping

On every render frame, the `WallSystem` component in [[`wall-system.tsx`](https://github.com/pascalorg/editor/blob/main/wall-system.tsx)](https://github.com/pascalorg/editor/blob/main/packages/core/src/systems/wall/wall-system.tsx) monitors the scene store for walls marked as *dirty*—indicating changes to geometry, position, or children. The system collects these IDs and groups them by their parent **level** node to process related walls together.

The main loop (lines 30-78) performs three critical operations:

1. **Gathering walls per level** – Iterates through `dirtyNodes` to map wall IDs to their parent level IDs
2. **Processing each level** – Calls `calculateLevelMiters` for all walls in affected levels
3. **Updating adjacent walls** – Refreshes neighboring walls that share junctions with modified walls (lines 52-66) to ensure miter continuity

```tsx
export const WallSystem = () => {
  const dirtyNodes = useScene(state => state.dirtyNodes);
  const clearDirty = useScene(state => state.clearDirty);

  useFrame(() => {
    if (dirtyNodes.size === 0) return;

    // Gather walls per level
    const dirtyWallsByLevel = new Map<string, Set<string>>();
    dirtyNodes.forEach(id => {
      const node = useScene.getState().nodes[id];
      if (node?.type !== 'wall' || !node.parentId) return;
      const levelId = node.parentId;
      if (!dirtyWallsByLevel.has(levelId))
        dirtyWallsByLevel.set(levelId, new Set());
      dirtyWallsByLevel.get(levelId)!.add(id);
    });

    // Process each level
    for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
      const levelWalls = getLevelWalls(levelId);
      const miterData = calculateLevelMiters(levelWalls);

      // Update the walls that changed
      dirtyWallIds.forEach(wallId => {
        const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh;
        if (mesh) {
          updateWallGeometry(wallId, miterData);
          clearDirty(wallId as AnyNodeId);
        }
      });

      // Also refresh neighbours that share a junction
      const adjacentIds = getAdjacentWallIds(levelWalls, dirtyWallIds);
      adjacentIds.forEach(id => {
        if (!dirtyWallIds.has(id)) {
          const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh;
          if (mesh) updateWallGeometry(id, miterData);
        }
      });
    }
  }, 4);        // run after the main scene update
  return null;
};

```

## Miter Calculation and Junction Detection

The mitering algorithm lives in [[`wall-mitering.ts`](https://github.com/pascalorg/editor/blob/main/wall-mitering.ts)](https://github.com/pascalorg/editor/blob/main/packages/core/src/systems/wall/wall-mitering.ts) and determines how wall thickness should be trimmed at intersections to create clean architectural joints.

### Detecting Wall Junctions

The `findJunctions` function (lines 84-127) indexes walls by their endpoints using `pointToKey` hashing. It identifies:

- **Corner junctions** – Where two walls meet at endpoints
- **T-junctions** – Where a wall end meets another wall's interior (detected via `pointOnWallSegment`)

Results are stored in a `Map<string, Junction>` where keys represent snapped 2-D coordinates.

### Computing Miter Offsets

For every junction, `calculateJunctionIntersections` (lines 42-98) builds offset lines for each participating wall:

1. **Edge creation** – Constructs left and right offset lines using the wall's normal vector (`nUnit`) and half-thickness
2. **Passthrough handling** – For T-junctions, generates edges in both directions so neighbors can miter against the intersected wall's middle
3. **Intersection calculation** – Sorts edges by outgoing angle and intersects successive pairs to determine trim points

The resulting `WallMiterData` contains a map of junction keys to per-wall intersection points used during extrusion.

## Extrusion and CSG Boolean Operations

The `generateExtrudedWall` function in [[`wall-system.tsx`](https://github.com/pascalorg/editor/blob/main/wall-system.tsx)](https://github.com/pascalorg/editor/blob/main/packages/core/src/systems/wall/wall-system.tsx) converts mitered 2-D polygons into 3-D geometry with door and window cut-outs.

### Building the Plan Footprint

Before extrusion, `getWallPlanFootprint` in [[`wall-footprint.ts`](https://github.com/pascalorg/editor/blob/main/wall-footprint.ts)](https://github.com/pascalorg/editor/blob/main/packages/core/src/systems/wall/wall-footprint.ts) (lines 11-60) constructs a closed polygon from the pre-computed miter data:

```ts
export function getWallPlanFootprint(wallNode: WallNode, miterData: WallMiterData): Point2D[] {
  const { junctionData } = miterData;
  const wallStart: Point2D = { x: wallNode.start[0], y: wallNode.start[1] };
  const wallEnd: Point2D   = { x: wallNode.end[0],   y: wallNode.end[1] };
  const thickness = getWallThickness(wallNode);
  const halfT = thickness / 2;

  const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
  const L = Math.sqrt(v.x * v.x + v.y * v.y);
  if (L < 1e-9) return [];

  const nUnit = { x: -v.y / L, y: v.x / L };
  const keyStart = pointToKey(wallStart);
  const keyEnd   = pointToKey(wallEnd);

  const startJunction = junctionData.get(keyStart)?.get(wallNode.id);
  const endJunction   = junctionData.get(keyEnd)  ?.get(wallNode.id);

  const pStartLeft  = startJunction?.left  || { x: wallStart.x + nUnit.x * halfT, y: wallStart.y + nUnit.y * halfT };
  const pStartRight = startJunction?.right || { x: wallStart.x - nUnit.x * halfT, y: wallStart.y - nUnit.y * halfT };
  const pEndLeft    = endJunction?.right   || { x: wallEnd.x   + nUnit.x * halfT, y: wallEnd.y   + nUnit.y * halfT };
  const pEndRight   = endJunction?.left    || { x: wallEnd.x   - nUnit.x * halfT, y: wallEnd.y   - nUnit.y * halfT };

  const polygon: Point2D[] = [pStartRight, pEndRight];
  if (endJunction) polygon.push(wallEnd);
  polygon.push(pEndLeft, pStartLeft);
  if (startJunction) polygon.push(wallStart);
  return polygon;
}

```

### Three.js Extrusion and Rotation

The wall geometry generation follows a coordinate transformation pipeline:

1. **Localization** – Rotates world-space points to align the wall length with the X-axis
2. **Shape creation** – Builds a `THREE.Shape` from the localized footprint points
3. **Vertical extrusion** – Uses `THREE.ExtrudeGeometry` with the wall height, then rotates the result (`rotateX(-Math.PI / 2)`) to align the extrusion with the world Y-axis

### Boolean Subtraction with three-bvh-csg

For architectural openings, the system uses **three-bvh-csg** to subtract door and window volumes:

- **`collectCutoutBrushes`** (lines 50-84 of [`wall-system.tsx`](https://github.com/pascalorg/editor/blob/main/wall-system.tsx)) creates thin box geometries spanning the full wall thickness for each child node marked as a cut-out
- **BVH optimization** – Both wall and cut-out geometries receive bounding-volume hierarchies via `computeBoundsTree` for fast CSG evaluation
- **Subtraction operation** – The `csgEvaluator.evaluate` function processes the wall brush against each cut-out brush using the `SUBTRACTION` operation

```ts
export function generateExtrudedWall(
  wallNode: WallNode,
  childrenNodes: AnyNode[],
  miterData: WallMiterData,
  slabElevation = 0,
) {
  // Build the plan-view polygon with correct miter offsets
  const polyPoints = getWallPlanFootprint(wallNode, miterData);
  if (polyPoints.length < 3) return new THREE.BufferGeometry();

  // Transform world-space points to wall-local space
  const wallAngle = Math.atan2(v.y, v.x);
  const cosA = Math.cos(-wallAngle), sinA = Math.sin(-wallAngle);
  const worldToLocal = (pt: Point2D) => {
    const dx = pt.x - wallStart.x, dy = pt.y - wallStart.y;
    return { x: dx * cosA - dy * sinA, z: dx * sinA + dy * cosA };
  };
  const localPoints = polyPoints.map(worldToLocal);

  // Create a Three.js Shape and extrude it vertically
  const footprint = new THREE.Shape();
  footprint.moveTo(localPoints[0]!.x, -localPoints[0]!.z);
  for (let i = 1; i < localPoints.length; i++) {
    footprint.lineTo(localPoints[i]!.x, -localPoints[i]!.z);
  }
  const geometry = new THREE.ExtrudeGeometry(footprint, { depth: height, bevelEnabled: false });
  geometry.rotateX(-Math.PI / 2);
  geometry.computeVertexNormals();

  // Gather door/window cut-outs and run CSG subtraction
  const cutoutBrushes = collectCutoutBrushes(wallNode, childrenNodes, thickness);
  if (cutoutBrushes.length === 0) return geometry;

  // Prepare BVH for fast CSG
  geometry.computeBoundsTree = computeBoundsTree;
  geometry.computeBoundsTree({ maxLeafSize: 10 });
  const wallBrush = new Brush(geometry);
  let resultBrush = wallBrush;
  for (const cutoutBrush of cutoutBrushes) {
    resultBrush = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
  }
  return resultBrush.geometry;
}

```

## Practical Implementation Examples

### Adding a Wall and Triggering Geometry Generation

```tsx
import useScene from '@pascal-app/core/store/use-scene';
import { v4 as uuid } from 'uuid';

// Create a wall node (start-end in world X-Z plane)
const newWall = {
  id: uuid(),
  type: 'wall' as const,
  parentId: levelId,                // the level the wall belongs to
  start: [0, 0],
  end: [5, 0],
  height: 2.8,
  thickness: 0.12,
  children: [],                     // doors/windows will be added later
};

// Insert into scene and mark dirty for processing
useScene.getState().addNode(newWall);
useScene.getState().markDirty(newWall.id);

```

### Adding a Window Cut-Out

```tsx
const windowNode = {
  id: uuid(),
  type: 'window' as const,
  parentId: newWall.id,
  start: [2, 0],
  end: [3, 0],
};

useScene.getState().addNode(windowNode);
useScene.getState().markDirty(newWall.id);   // triggers CSG subtraction

```

### Manual Re-Mitering for Global Changes

When updating wall thickness globally without individual node changes:

```ts
import { calculateLevelMiters } from '@pascal-app/core/systems/wall/wall-mitering';
import { getLevelWalls } from '@pascal-app/core/systems/wall/wall-system';

const levelId = 'level-abc';
const walls = getLevelWalls(levelId);
const miterData = calculateLevelMiters(walls);

// Apply new miter data to every wall in the level
walls.forEach(w => updateWallGeometry(w.id, miterData));

```

## Summary

- **Dirty-node tracking** in [`wall-system.tsx`](https://github.com/pascalorg/editor/blob/main/wall-system.tsx) groups changed walls by level and processes them in frame-priority order 4, ensuring scene consistency before geometry generation.
- **Miter calculation** uses vector math and intersection detection in [`wall-mitering.ts`](https://github.com/pascalorg/editor/blob/main/wall-mitering.ts) to compute exact offset points for clean wall junctions, handling both corners and T-junctions.
- **Plan footprint generation** in [`wall-footprint.ts`](https://github.com/pascalorg/editor/blob/main/wall-footprint.ts) constructs closed 2-D polygons that incorporate pre-computed miter offsets for both wall ends.
- **CSG pipeline** utilizes three-bvh-csg with BVH acceleration to subtract door and window volumes from extruded wall meshes in real-time.
- **Adjacent wall updates** ensure that modifying one wall triggers recomputation of neighbors sharing junctions, maintaining geometric continuity across the level.

## Frequently Asked Questions

### How does the Pascal Editor handle T-junctions in wall geometry?

The `findJunctions` function in [`wall-mitering.ts`](https://github.com/pascalorg/editor/blob/main/wall-mitering.ts) detects T-junctions by checking if a wall endpoint lies on another wall's interior segment using `pointOnWallSegment`. For these cases, the algorithm marks the intersected wall as a "passthrough" and generates offset edges in both directions, allowing the connecting wall to miter correctly against the middle of the intersected wall rather than its endpoint.

### What CSG library does Pascal Editor use for wall cut-outs?

The system uses **three-bvh-csg**, an optimized constructive solid geometry library that leverages bounding volume hierarchies (BVH) for fast Boolean operations. Both the wall geometry and cut-out brushes receive BVH structures via `computeBoundsTree` before evaluation, enabling real-time subtraction of complex door and window openings even in large architectural scenes.

### How does the wall system optimize performance for complex scenes?

Performance optimization occurs at multiple stages: **level-based grouping** limits miter calculations to affected floors only; **dirty-node tracking** ensures only modified walls and their immediate neighbors are recomputed; and **BVH acceleration** in the CSG pipeline reduces the complexity of Boolean operations from O(n²) to O(log n) for mesh intersections.

### Can wall thickness be updated dynamically without rebuilding the entire scene?

Yes. The `calculateLevelMiters` function can be invoked manually with the current wall set to regenerate miter data based on new thickness values. Calling `updateWallGeometry` for each wall in the level applies the new offsets without requiring individual node dirty flags, though all affected walls must be reprocessed to maintain proper junction geometry.