xeokit Section Caps


Generates filled cap geometry along arbitrary world-space cut planes through a SceneModel, so sliced models read as solid cross-sections instead of hollow shells.


The sectionCaps module complements SectionPlane-style clipping: where a SectionPlane hides everything on the clipped side of a plane, sectionCaps fills the hole the plane leaves behind. Output is embedded into a caller-owned target SceneModel as flat TrianglesPrimitive meshes lying on the cut planes, one SceneObject per source object that contributed at least one cap.


%%{init:{"theme":"dark"}}%% classDiagram direction TB class buildSectionCaps { +call(params) } class canBuildSectionCaps { +call(sourceModel) boolean } class clearSectionCaps { +call(targetModel) } class BuildSectionCapsParams { +sourceModel : SceneModel +targetModel : SceneModel +capPlanes : CapPlane[] +capColor : Vec3 +idPrefix : string +layerId : string +includeObjectIds : string[] +excludeObjectIds : string[] +includeObject(obj) boolean +progressive : ProgressiveSpec } class CapPlane { +dir : Vec3 +dist : number } class ProgressiveSpec { +batchSize : number +yield() Promise } class BuildSectionCapsResult { +numObjectsWithCaps : number +numCapMeshes : number +numUnclosedMeshes : number } class SceneModel { <<scene>> } class SectionPlane { <<viewer>> +dir : Vec3 +dist : number } BuildSectionCapsParams "1" *-- "*" CapPlane : capPlanes BuildSectionCapsParams o-- SceneModel : sourceModel BuildSectionCapsParams o-- SceneModel : targetModel BuildSectionCapsParams o-- ProgressiveSpec : progressive buildSectionCaps ..> BuildSectionCapsParams : reads buildSectionCaps ..> BuildSectionCapsResult : returns canBuildSectionCaps ..> SceneModel : scans clearSectionCaps ..> SceneModel : destroys CapPlane <.. SectionPlane : copy dir + dist
%%{init:{"theme":"default"}}%% classDiagram direction TB class buildSectionCaps { +call(params) } class canBuildSectionCaps { +call(sourceModel) boolean } class clearSectionCaps { +call(targetModel) } class BuildSectionCapsParams { +sourceModel : SceneModel +targetModel : SceneModel +capPlanes : CapPlane[] +capColor : Vec3 +idPrefix : string +layerId : string +includeObjectIds : string[] +excludeObjectIds : string[] +includeObject(obj) boolean +progressive : ProgressiveSpec } class CapPlane { +dir : Vec3 +dist : number } class ProgressiveSpec { +batchSize : number +yield() Promise } class BuildSectionCapsResult { +numObjectsWithCaps : number +numCapMeshes : number +numUnclosedMeshes : number } class SceneModel { <<scene>> } class SectionPlane { <<viewer>> +dir : Vec3 +dist : number } BuildSectionCapsParams "1" *-- "*" CapPlane : capPlanes BuildSectionCapsParams o-- SceneModel : sourceModel BuildSectionCapsParams o-- SceneModel : targetModel BuildSectionCapsParams o-- ProgressiveSpec : progressive buildSectionCaps ..> BuildSectionCapsParams : reads buildSectionCaps ..> BuildSectionCapsResult : returns canBuildSectionCaps ..> SceneModel : scans clearSectionCaps ..> SceneModel : destroys CapPlane <.. SectionPlane : copy dir + dist
classDiagram
    direction TB
    class buildSectionCaps {
      +call(params)
    }
    class canBuildSectionCaps {
      +call(sourceModel) boolean
    }
    class clearSectionCaps {
      +call(targetModel)
    }
    class BuildSectionCapsParams {
      +sourceModel : SceneModel
      +targetModel : SceneModel
      +capPlanes : CapPlane[]
      +capColor : Vec3
      +idPrefix : string
      +layerId : string
      +includeObjectIds : string[]
      +excludeObjectIds : string[]
      +includeObject(obj) boolean
      +progressive : ProgressiveSpec
    }
    class CapPlane {
      +dir : Vec3
      +dist : number
    }
    class ProgressiveSpec {
      +batchSize : number
      +yield() Promise
    }
    class BuildSectionCapsResult {
      +numObjectsWithCaps : number
      +numCapMeshes : number
      +numUnclosedMeshes : number
    }
    class SceneModel {
      <<scene>>
    }
    class SectionPlane {
      <<viewer>>
      +dir : Vec3
      +dist : number
    }
    BuildSectionCapsParams "1" *-- "*" CapPlane : capPlanes
    BuildSectionCapsParams o-- SceneModel : sourceModel
    BuildSectionCapsParams o-- SceneModel : targetModel
    BuildSectionCapsParams o-- ProgressiveSpec : progressive
    buildSectionCaps ..> BuildSectionCapsParams : reads
    buildSectionCaps ..> BuildSectionCapsResult : returns
    canBuildSectionCaps ..> SceneModel : scans
    clearSectionCaps ..> SceneModel : destroys
    CapPlane <.. SectionPlane : copy dir + dist

  • Watertight cap fills — every cut produces a closed, triangulated polygon (per source mesh per plane) so the model reads as a solid cross-section rather than a hollow shell.
  • Multi-plane caps — caps from intersecting cut planes are trimmed against each other's kept half-spaces (Sutherland-Hodgman), so two cuts don't double-cover their shared corner.
  • Outer / hole topology — loops are classified by signed area and holes attached to their tightest container, so cross-sections through hollow members (pipes, walls with openings) render with their interiors open rather than filled.
  • Hatch-pattern preservation — the cap's SceneMaterial inherits the source mesh material's hatchPattern (when present), so sectioned masonry, concrete, and metal read with the same engineering hatch convention as the rest of the model.
  • Per-mesh colour passthrough — when no capColor is supplied, each cap inherits its source mesh's effective colour (material colour first, then mesh-level colour, then a neutral grey fallback). Reads better on multi-material BIM models than a single global tint.
  • SectionPlane-compatible conventionCapPlane.dir and CapPlane.dist match the accessors on a viewer-side SectionPlane verbatim, so a cap can be built directly from any clipping plane the host already drives.
  • Cross-Scene — source and target SceneModels may live in different Scenes.
  • Object filtering — restrict the cap pass with includeObjectIds, excludeObjectIds, and/or an arbitrary includeObject predicate. Right for "cap walls + slabs but not furniture" or "cap whatever's visible right now."
  • ViewLayer parking — pass layerId to assign every emitted SceneObject to a named ViewLayer; the host can then hide, show, or style caps collectively.
  • Progressive emission — yield between batches so very large models cap progressively without blocking the main thread.
  • Diagnostic countsnumUnclosedMeshes flags non-watertight source geometry that produced no cap.

npm install @xeokit/sdk

The snippets below assume a Scene with a source SceneModel already loaded (via any SDK importer — for example, DotBIMLoader or IFCLoader).


import {
buildSectionCaps,
canBuildSectionCaps,
clearSectionCaps
} from "@xeokit/sdk/presentations/sectionCaps";

The caller creates the target SceneModel and passes it in — buildSectionCaps populates it. The plane convention matches SectionPlane: the half-space dot(dir, p) + dist > 0 is clipped (discarded) and the cap's face normal is +dir.

const sourceModel = scene.models["myModel"];
const targetModel = scene.createModel({ id: "myModel__caps" }).value;

const result = await buildSectionCaps({
sourceModel,
targetModel,
capPlanes: [
{ dir: [0, 1, 0], dist: -1.2 } // horizontal cut at y = 1.2
]
});

if (!result.ok) {
console.error(result.error);
targetModel.destroy(); // partial state may exist
} else {
const { numObjectsWithCaps, numCapMeshes } = result.value;
console.log(`${numCapMeshes} caps across ${numObjectsWithCaps} objects`);
}

Pass several planes to cut along independent axes. Each plane produces its own cap set per source object, trimmed against the kept half-space of every other plane so intersecting cuts don't overlap at their shared corner.

await buildSectionCaps({
sourceModel,
targetModel,
capPlanes: [
{ dir: [0, 1, 0], dist: -1.2 }, // floor cut
{ dir: [1, 0, 0], dist: -5.0 }, // east-west cut
{ dir: [0, 0, 1], dist: -10.0 } // north-south cut
]
});

CapPlane mirrors the dir / dist accessors on a SectionPlane, so the host's existing clipping plane can be reused verbatim — cap geometry lands on the same plane the viewer is already clipping against.

const sectionPlane = view.sectionPlanes["floor"];

await buildSectionCaps({
sourceModel,
targetModel,
capPlanes: [
{ dir: sectionPlane.dir, dist: sectionPlane.dist }
]
});

idPrefix namespaces every emitted geometry, material, mesh, and object id ("{idPrefix}__{sourceObjectId}__…"). Pick a unique prefix per pass when running the extractor more than once into the same target — e.g. one prefix per cap-plane set, or one per source model when capping a federation against the same plane.

await buildSectionCaps({
sourceModel: scene.models["architectural"],
targetModel,
capPlanes,
idPrefix: "cap_arch"
});

await buildSectionCaps({
sourceModel: scene.models["structural"],
targetModel,
capPlanes,
idPrefix: "cap_struct"
});

capColor sets the RGB base colour of every emitted cap SceneMaterial. Omit capColor to inherit each cap's tint from its source mesh's effective colour (material colour first, then mesh-level colour, then a neutral grey fallback) — useful on multi-material BIM models where a single global tint would erase material differences. When the corresponding source mesh's material carries a hatchPattern, the cap material inherits it — sectioned masonry, concrete, or metal then renders with the same engineering hatch as the rest of the model.

// Inherit per-mesh colour from the source material:
await buildSectionCaps({ sourceModel, targetModel, capPlanes });

// Or override every cap with a uniform paper-white:
await buildSectionCaps({
sourceModel, targetModel, capPlanes,
capColor: [0.92, 0.93, 0.95]
});

Pass layerId to assign every emitted SceneObject to a named ViewLayer. The host can then hide, show, or restyle the caps collectively without touching the source model.

await buildSectionCaps({
sourceModel,
targetModel,
capPlanes,
layerId: "sectionCaps"
});

Restrict the cap pass with any combination of an id whitelist, an id blacklist, and an arbitrary predicate. All three conditions AND together when present.

// Cap only specific objects:
await buildSectionCaps({
sourceModel, targetModel, capPlanes,
includeObjectIds: ["wall_01", "wall_02", "slab_03"]
});

// Cap everything except a transient hidden set (large set ->
// prefer Set membership):
const hidden = new Set(currentlyHiddenIds);
await buildSectionCaps({
sourceModel, targetModel, capPlanes,
excludeObjectIds: hidden
});

// Predicate filter — "cap walls + slabs only", from the semantic
// Data graph:
await buildSectionCaps({
sourceModel, targetModel, capPlanes,
includeObject: (obj) => {
const ifcType = data.objects[obj.id]?.type;
return ifcType === "IfcWallStandardCase"
|| ifcType === "IfcSlab";
}
});

// Predicate filter — "cap whatever's visible in this View":
await buildSectionCaps({
sourceModel, targetModel, capPlanes,
includeObject: (obj) => view.objects[obj.id]?.visible === true
});

For large source models, pass progressive: true so the function yields between batches of source-object emissions and the caps paint into the View as they build. Pass a ProgressiveSpec for finer control.

await buildSectionCaps({
sourceModel,
targetModel,
capPlanes,
progressive: {
batchSize: 50, // source objects per yield
yield: () => new Promise(resolve =>
requestAnimationFrame(() => resolve())
)
}
});

The function only reads source geometry and writes to the target, so the two SceneModels may live in different Scenes. Useful when capping a federation loaded in one Scene into a "drawings" Scene that the host renders on a separate canvas / view.

const liveScene     = new Scene();
const drawingsScene = new Scene();

// ... load the building into liveScene["building"] ...

const capTarget = drawingsScene.createModel({ id: "caps" }).value;

await buildSectionCaps({
sourceModel: liveScene.models["building"],
targetModel: capTarget,
capPlanes: [{ dir: [0, 1, 0], dist: -1.2 }]
});

Point clouds and line-only models produce no caps. canBuildSectionCaps reports whether the source has any triangle-bearing geometry the cap builder can usefully consume, so callers can skip the call instead of running it for nothing.

if (canBuildSectionCaps(sourceModel)) {
const targetModel = scene.createModel({ id: "caps" }).value;
await buildSectionCaps({ sourceModel, targetModel, capPlanes });
}

BuildSectionCapsResult reports per-pass counts. The numUnclosedMeshes field is a diagnostic — non-zero means the source had non-watertight meshes whose cap segments didn't stitch into closed loops. Cap coverage degrades on those meshes; meshes with stitched loops still produce caps.

const result = await buildSectionCaps({ / * ... * / });

if (result.ok) {
const { numObjectsWithCaps, numCapMeshes, numUnclosedMeshes } = result.value;

if (numUnclosedMeshes > 0) {
console.warn(`${numUnclosedMeshes} source meshes were non-watertight`);
}
}

The caller owns the target SceneModel, so teardown is just targetModel.destroy(). clearSectionCaps is a convenience wrapper that no-ops if the target is null or already destroyed. Running buildSectionCaps repeatedly into a freshly-created target lets the host swap cap sets as the user moves the cut plane.

clearSectionCaps(targetModel);

Interfaces

BuildSectionCapsParams
BuildSectionCapsResult
CapPlane
ProgressiveSpec

Functions

buildSectionCaps
canBuildSectionCaps
clearSectionCaps