Pascal Editor Roof Geometry Generation with Segments: A Technical Deep Dive
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) 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– DefinesRoofNode, which holdsposition,rotation, and an array ofchildren: RoofSegmentNode[]. This node has no geometry of its own; it functions purely as a grouping mechanism.packages/core/src/schema/nodes/roof-segment.ts– DefinesRoofSegmentNode, which stores geometric parameters includingroofType,width,depth,wallHeight,roofHeight,wallThickness,overhang,shingleThickness, anddeckThickness.packages/core/src/store/use-scene.ts– The Zustand-based global state management that tracksnodes,dirtyNodes, androotNodeIds. It exposesmarkDirty(id)to invalidate geometry andclearDirty(id)to signal completion.packages/core/src/hooks/scene-registry/scene-registry.ts– Maps node IDs to live Three.js objects (THREE.MeshorTHREE.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:
getModuleFaces– Computes the planar faces for the selectedroofType(gable, hip, etc.) based on the segment’s width, depth, and height parameters.createGeometryFromFaces– Converts the calculated faces into aTHREE.BufferGeometry.getRoofSegmentBrushes– Transforms the geometry intoBrushobjects (usingthree-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:
- Retrieves every child segment and transforms each brush into world space using
_matrix.compose(position, rotation, scale). - Cumulatively adds brushes (
ADDoperation) to produce four aggregated volumes:totalShinSlab,totalDeckSlab,totalWall, andtotalInner. - Performs CSG subtractions to carve the inner cavity from walls and trim deck/shingles against the interior volume.
- Merges the results into a single
THREE.Meshnamedmerged-roof. - Remaps material indices via
mapRoofGroupMaterialIndexto 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:
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:
// 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:
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:
RoofNodecontainers andRoofSegmentNodegeometry modules defined inpackages/core/src/schema/nodes/. - CSG operations in
packages/core/src/systems/roof/roof-system.tsxgenerate hollow walls and shingle volumes usingthree-bvh-csgbrushes. - Dirty detection via
markDirty()in the Zustand store (use-scene.ts) ensures geometry regenerates only for changed nodes. - Merged rendering combines all segments into a single
merged-roofmesh, 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.
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 →