Skip to main content

Overview

The WindowSystem is a React component that processes dirty window nodes and generates 3D window geometry. It supports:
  • Parametric window geometry with configurable pane grids
  • CSG cutouts in parent walls for window openings
  • Multi-pane layouts with independent row and column divisions
  • Optional sill with configurable depth and thickness
  • Flexible divider spacing for custom pane arrangements
The system runs in a useFrame hook at priority 3, ensuring windows update after items and slabs but before wall CSG operations.

Location

~/workspace/source/packages/core/src/systems/window/window-system.tsx

How It Works

Processing Pipeline

1

Monitor dirty nodes

The system watches the dirtyNodes set for nodes with type === 'window'.
2

Update window geometry

For each dirty window:
  • Generate new geometry from window parameters
  • Create frame members (top, bottom, left, right)
  • Build pane grid with column and row dividers
  • Add glass panes for each grid cell
  • Add optional sill
  • Update the invisible cutout mesh for wall CSG
3

Mark parent wall dirty

Trigger parent wall rebuild so its CSG cutout reflects the updated window geometry.
4

Clear dirty flag

Once the mesh is updated, the node is removed from dirtyNodes.

Geometry Generation

The updateWindowMesh() function builds window geometry in these steps:
  1. Create invisible hitbox (root mesh for selection)
  2. Build frame members (top, bottom, left, right)
  3. Calculate pane grid layout from columnRatios and rowRatios
  4. Add column dividers (full inner height)
  5. Add row dividers (per column width to avoid overlap)
  6. Add glass panes for each grid cell
  7. Add optional sill (protrudes from front face)
  8. Update cutout mesh (always full window dimensions, 1m deep for wall CSG)

Props

The WindowSystem component has no props. It reads state from the Zustand store:
dirtyNodes
Set<AnyNodeId>
Set of node IDs that need geometry updates
nodes
Record<AnyNodeId, AnyNode>
All nodes in the scene graph

Dependencies

WindowNode Schema

The system processes WindowNode objects with these properties:
id
string
required
Unique window identifier
position
[number, number, number]
default:"[0, 0, 0]"
Center of the window in wall-local coordinates [x, y, z]
rotation
[number, number, number]
default:"[0, 0, 0]"
Euler rotation in radians [x, y, z]
width
number
default:"1.5"
Overall window width in meters
height
number
default:"1.5"
Overall window height in meters
frameThickness
number
default:"0.05"
Width of frame members in meters
frameDepth
number
default:"0.07"
Depth of frame in meters (perpendicular to wall)
columnRatios
number[]
default:"[1]"
Relative width proportions for columns (e.g., [1, 1] = two equal columns)
rowRatios
number[]
default:"[1]"
Relative height proportions for rows (e.g., [1, 2] = bottom row twice as tall)
columnDividerThickness
number
default:"0.03"
Thickness of vertical dividers in meters
rowDividerThickness
number
default:"0.03"
Thickness of horizontal dividers in meters
sill
boolean
default:"true"
Whether to show window sill
sillDepth
number
default:"0.08"
Depth of sill protrusion in meters
sillThickness
number
default:"0.03"
Thickness of sill in meters
parentId
string
Parent wall ID

External Systems

  • Scene Registry (sceneRegistry.nodes): Maps node IDs to THREE.js meshes
  • Wall System (WallSystem): Applies CSG subtraction using window’s cutout mesh

Window Geometry Details

Frame Construction

The frame consists of four members:
  1. Top bar: Full width, frameThickness tall
  2. Bottom bar: Full width, frameThickness tall
  3. Left post: Inner height (to avoid corner overlap), frameThickness wide
  4. Right post: Inner height (to avoid corner overlap), frameThickness wide
const innerW = width - 2 * frameThickness
const innerH = height - 2 * frameThickness

// Top / bottom — full width
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0,  height / 2 - frameThickness / 2, 0)
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0)

// Left / right — inner height
addBox(mesh, frameMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0)
addBox(mesh, frameMaterial, frameThickness, innerH, frameDepth,  width / 2 - frameThickness / 2, 0, 0)

Pane Grid Layout

The inner area is divided by columnRatios and rowRatios:
1

Calculate usable dimensions

Subtract divider thicknesses from inner dimensions:
const usableW = innerW - (numCols - 1) * columnDividerThickness
const usableH = innerH - (numRows - 1) * rowDividerThickness
2

Distribute by ratios

Divide usable space proportionally:
const colSum = columnRatios.reduce((a, b) => a + b, 0)
const rowSum = rowRatios.reduce((a, b) => a + b, 0)
const colWidths  = columnRatios.map(r => (r / colSum) * usableW)
const rowHeights = rowRatios.map(r => (r / rowSum) * usableH)
3

Calculate cell centers

Compute X and Y centers for each grid cell accounting for dividers.

Divider Placement (window-system.tsx:133-150)

Column dividers span full inner height:
for (let c = 0; c < numCols - 1; c++) {
  addBox(mesh, frameMaterial, columnDividerThickness, innerH, frameDepth, x, 0, 0)
}
Row dividers are placed per column width to avoid overlapping column dividers:
for (let r = 0; r < numRows - 1; r++) {
  for (let c = 0; c < numCols; c++) {
    addBox(mesh, frameMaterial, colWidths[c], rowDividerThickness, frameDepth, colXCenters[c], y, 0)
  }
}

Glass Panes (window-system.tsx:152-158)

Each grid cell gets a thin glass pane:
const glassDepth = Math.max(0.004, frameDepth * 0.08)
for (let c = 0; c < numCols; c++) {
  for (let r = 0; r < numRows; r++) {
    addBox(mesh, glassMaterial, colWidths[c], rowHeights[r], glassDepth, colXCenters[c], rowYCenters[r], 0)
  }
}

Sill (window-system.tsx:160-166)

Optional sill protrudes from the front face:
if (sill) {
  const sillW = width + sillDepth * 0.4  // slightly wider than frame
  const sillZ = frameDepth / 2 + sillDepth / 2  // protrudes forward
  addBox(mesh, frameMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ)
}

CSG Cutouts

The window creates a cutout in its parent wall using Constructive Solid Geometry:
1

Create cutout mesh

Invisible mesh named 'cutout' with full window dimensions (width × height × 1.0m deep).
2

Wall CSG subtraction

Parent wall system subtracts the cutout brush from wall geometry using three-bvh-csg.
3

Rebuild wall on window changes

Whenever a window is updated, its parent wall is marked dirty to trigger CSG recalculation.
// Create cutout for wall CSG (window-system.tsx:168-177)
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
if (!cutout) {
  cutout = new THREE.Mesh()
  cutout.name = 'cutout'
  mesh.add(cutout)
}
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
cutout.visible = false

Usage in Viewer

import { WindowSystem } from '@pascal/core/systems/window/window-system'
import { useScene } from '@pascal/core/store/use-scene'

function createWindow() {
  const { addNode, markDirty } = useScene.getState()
  
  const window = {
    id: 'window_1',
    type: 'window',
    parentId: 'wall_1',
    position: [2.5, 1.5, 0],
    rotation: [0, 0, 0],
    width: 1.5,
    height: 1.5,
    frameThickness: 0.05,
    frameDepth: 0.07,
    columnRatios: [1, 1],  // Two equal columns
    rowRatios: [1],        // Single row
    columnDividerThickness: 0.03,
    rowDividerThickness: 0.03,
    sill: true,
    sillDepth: 0.08,
    sillThickness: 0.03,
  }
  
  addNode(window)
  markDirty(window.id)
}

function Viewer() {
  return (
    <Canvas>
      {/* Other scene content */}
      <WindowSystem />
    </Canvas>
  )
}
The system automatically:
  • Monitors the dirtyNodes store
  • Updates windows when they’re marked dirty
  • Triggers parent wall CSG updates

Common Window Configurations

Single-Hung Window

const singleHung = {
  columnRatios: [1],
  rowRatios: [1, 1],  // Two equal rows (top/bottom sashes)
  rowDividerThickness: 0.03,
}

Double-Hung Window

const doubleHung = {
  columnRatios: [1],
  rowRatios: [1, 1],
  rowDividerThickness: 0.025,  // Thinner meeting rail
}

Bay Window Center

const bayCenter = {
  columnRatios: [1, 1, 1],  // Three equal columns
  rowRatios: [0.3, 0.7],    // Smaller top row (transom)
  columnDividerThickness: 0.05,
  rowDividerThickness: 0.03,
}

Picture Window

const picture = {
  columnRatios: [1],  // No divisions
  rowRatios: [1],
  width: 2.5,
  height: 2.0,
}

Casement Pair

const casementPair = {
  columnRatios: [1, 1],  // Two equal casements
  rowRatios: [1],
  columnDividerThickness: 0.05,  // Center mullion
}

Materials

The system uses three materials:
const glassMaterial = new MeshStandardNodeMaterial({
  color: 'lightblue',
  roughness: 0.05,
  metalness: 0.1,
  transparent: true,
  opacity: 0.3,
  side: DoubleSide,
  depthWrite: false,
})

const frameMaterial = new MeshStandardNodeMaterial({
  color: '#e8e8e8',  // Light gray for frame
  roughness: 0.6,
  metalness: 0,
})

const hitboxMaterial = new THREE.MeshBasicMaterial({
  visible: false,  // Invisible root mesh for selection
})

Performance Notes

  • Only updates dirty windows (not all windows every frame)
  • Reuses shared material instances across all windows
  • Disposes old geometries to prevent memory leaks
  • Root mesh is invisible hitbox; all visuals are child meshes
  • Runs at priority 3 (after items, before wall CSG)

Coordinate System

Windows use wall-local coordinates:
  • X-axis: Along wall length
  • Y-axis: Height (up)
  • Z-axis: Perpendicular to wall (through thickness)
The window’s position is its center in wall-local space.

Integration with Other Systems

Wall System

Walls query child window nodes to:
  • Collect cutout meshes named 'cutout'
  • Apply CSG subtraction to create window openings
  • Maintain wall structural integrity around openings

Scene Registry

All window meshes are registered in sceneRegistry.nodes for quick lookup by ID and selection.

Build docs developers (and LLMs) love