Skip to content

Why a Family Layer

The CSG IR is deliberately anonymous. Two identical wall recipes hash identically, share one cache entry, and materialize once. That is exactly what you want from a geometry cache, and exactly what you cannot ship to a BIM consumer: a building needs every wall to keep its own name, its own properties, and its own stable identity across exports.

brepjs-families resolves that tension by adding a second tree beside the IR, not inside it. You describe a model as a tree of elements, each optionally carrying a key, properties, and property sets. Resolution projects every element onto the content-addressed IR for geometry, while identity rides on the element tree and never enters a cache key. Identical recipes still share one materialization; identities stay distinct.

Try it live: the Declarative Building playground example runs this layer in the browser, and the playground's Families category has four more, including a projection to IFC and a steel frame covering columns, beams, roofs and stairs.

typescript
import { family, el, resolve, evaluateModel, type Element } from 'brepjs-families';
import { csg } from 'brepjs';

const Wall = family<{ readonly length: number; readonly height: number }>('Wall', (p) =>
  el('Box', { size: [p.length, 200, p.height] })
);

const Storey = family<{ readonly walls: readonly Element[] }>('Storey', (p) =>
  el('Group', {}, p.walls)
);

const model = resolve(
  Storey({
    key: 'ground',
    walls: [
      Wall({ key: 'north', length: 4000, height: 2700 }),
      Wall({ key: 'south', length: 4000, height: 2700 }),
    ],
  })
);
// Two elements, two key paths: 'ground/north' and 'ground/south'.
// One IR recipe underneath: both walls share a single cache entry.

A family() is a plain function from props to an element tree. There is no React, no reconciler, no lifecycle: calling a family builds a description, and resolve() runs the render functions to produce a ResolvedElement tree. If you prefer JSX, the package ships a jsx-runtime export and the plain-function API stays primary.

What a family carries

ConcernWhere it lives
Geometry recipeThe rendered IR subtree (content-addressed, cached)
IdentityThe element's key path (ground/north)
Propertiesprops, validated by an optional Zod schema
Property sets, materialsattributes, captured beside the geometry
Containment and openingsrelationships derived during resolution

The split is strict by construction: the IR node under an element carries none of the identity, so nothing you attach to an element can perturb geometry hashing or fragment the cache.

The intrinsic vocabulary

Render functions bottom out in a small set of intrinsic elements:

  • Box (size) and Cylinder (radius, height): the common building primitives.
  • Group / Fragment: pure containers; structure and identity, no geometry.
  • Geometry (node): the bridge to the full csg vocabulary. A render function builds any IR node (a profile extruded into a wall, a revolve, a boolean composition) and hands it over; voids, fuse, and transform compose on top exactly as they do for Box.

The vocabulary stays deliberately small because Geometry makes it complete: anything the csg builders can express, a family can render.

Evaluation is mesh-first

evaluateModel walks the resolved tree and returns one record per geometry-bearing element. The primary output is a mesh: plain data with no kernel lifetimes to manage, cached by content so a re-evaluation after an edit only re-meshes what changed.

typescript
using evaluator = new csg.Evaluator();
const evaluated = evaluateModel(model, evaluator);
for (const [keyPath, node] of evaluated.byKeyPath) {
  if (node.mesh.ok) console.log(keyPath, node.mesh.value.triangles.length / 3, 'triangles');
}

B-Rep shape handles are opt-in ({ shapes: true }) for export paths. Viewport consumers never touch a handle, which means there are no disposal rules to get wrong in rendering code.

Copy-in, not framework

Families are distributed on the copy-in model: npx brepjs add wall door writes family source files into your project, where you own and edit them. The package supplies the vocabulary (family, el, resolve, evaluateModel); the components are yours. The boundary is deliberate:

Ships as owned sourceStays in the package
Render functions (props to elements)IR node construction and evaluation
Default property sets, namingIFC entity writing
Material assignmentGlobalId derivation
Which geometry recipe to useSpatial aggregation, relationship emission

A family that needs to reimplement IFC writing means the boundary was drawn wrong; that capability belongs in brepjs-bim.

Installing

sh
npm install brepjs-families

The package ships a built ESM and CJS bundle with TypeScript declarations, so it works in any bundler or Node setup without extra configuration. npm create brepjs scaffolds a ready-to-run project around it.

Next steps

  • Your First Building: a storey with walls and a door, evaluated to meshes, with a prop edit that shows the cache sparing unchanged siblings.
  • Elements, Key Paths & Identity: the mental model in depth, including why unkeyed elements cannot mint identity.

Released under the Apache 2.0 License.