Pascal Editor Wall System Geometry Generation: Mitering and CSG Pipeline
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/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:
- Gathering walls per level – Iterates through
dirtyNodesto map wall IDs to their parent level IDs - Processing each level – Calls
calculateLevelMitersfor all walls in affected levels - Updating adjacent walls – Refreshes neighboring walls that share junctions with modified walls (lines 52-66) to ensure miter continuity
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/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:
- Edge creation – Constructs left and right offset lines using the wall's normal vector (
nUnit) and half-thickness - Passthrough handling – For T-junctions, generates edges in both directions so neighbors can miter against the intersected wall's middle
- 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/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/packages/core/src/systems/wall/wall-footprint.ts) (lines 11-60) constructs a closed polygon from the pre-computed miter data:
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:
- Localization – Rotates world-space points to align the wall length with the X-axis
- Shape creation – Builds a
THREE.Shapefrom the localized footprint points - Vertical extrusion – Uses
THREE.ExtrudeGeometrywith 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 ofwall-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
computeBoundsTreefor fast CSG evaluation - Subtraction operation – The
csgEvaluator.evaluatefunction processes the wall brush against each cut-out brush using theSUBTRACTIONoperation
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
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
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:
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.tsxgroups 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.tsto compute exact offset points for clean wall junctions, handling both corners and T-junctions. - Plan footprint generation in
wall-footprint.tsconstructs 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →