Thanks for your interest in contributing. This guide covers how the repo is laid out, how to run it locally, and a few gotchas that aren't obvious from reading the code.
For general guidelines shared across all That Open Company repos (ask-first policy, conventional commits, JSDoc rules, example code rules), read docs.thatopen.com/contributing first. This file only covers what's specific to engine_components.
@thatopen/components is a modular BIM toolkit built on top of @thatopen/fragments and Three.js. It provides the building blocks for authoring BIM web apps: scene/camera/renderer bundles (Worlds), model loading and classification, clipping, raycasting, measurements, property viewers, and so on. Each feature is an independent Component you opt into.
The repo ships two packages:
@thatopen/components(inpackages/core/) — renderer-agnostic base. Safe to use in headless or server-side workflows.@thatopen/components-front(inpackages/front/) — front-end additions that assume a rendered scene (post-production renderer, highlighter, outliner, markers, etc.).
packages/
├── core/src/ # @thatopen/components
│ ├── core/
│ │ ├── Components/ # Root container: singleton registry + animation loop
│ │ ├── Worlds/ # Scene + camera + renderer bundles
│ │ ├── Types/ # Base classes: Component, Disposable, Event, DataMap, World
│ │ ├── ConfigManager/ # Typed config with diff + apply
│ │ ├── Clipper/ # Section planes
│ │ ├── Grids/ # Ground grid
│ │ ├── Raycasters/ # Mouse / screen raycasting helpers
│ │ ├── OrthoPerspectiveCamera/
│ │ ├── FastModelPicker/ # GPU-based picking
│ │ ├── ShadowedScene/
│ │ ├── Viewpoints/ # Named camera states
│ │ ├── Views/ # 2D technical views
│ │ └── Disposer/
│ ├── fragments/ # Fragments integration (thin wrappers on @thatopen/fragments)
│ │ ├── FragmentsManager/ # Loads & owns FragmentsModels
│ │ ├── IfcLoader/ # IFC → .frag via IfcImporter
│ │ ├── Classifier/ # Group items by category / attributes
│ │ ├── Hider/ # Visibility management
│ │ ├── BoundingBoxer/ # Per-item AABB queries
│ │ ├── ItemsFinder/ # Query items by rule sets
│ │ └── EdgeProjector/ # Silhouette/edge extraction
│ ├── measurement/ # Distance, area, angle
│ ├── drawings/ # 2D drawing generation from 3D models
│ ├── openbim/ # BCF topics, IDS specifications
│ └── utils/ # Math, UUID, helpers
└── front/src/ # @thatopen/components-front (adds renderer features)
├── core/
│ ├── PostproductionRenderer/ # N8AO + edges + shadows
│ ├── ClipStyler/ # Filled clipping sections
│ ├── Marker/ # 3D labels
│ ├── PlatformComponents/ # Floating UI bits
│ └── DepthDebugRenderer/
├── fragments/
│ ├── Highlighter/ # Click-to-highlight items
│ ├── Hoverer/ # Hover events
│ ├── Outliner/ # Per-item outlines
│ └── Mesher/ # Extract mesh from fragment items
├── civil/ # Alignment viewers (road / rail)
├── drawings/ # Front-end drawing helpers
├── measurement/ # Renderer-aware measurement UIs
└── utils/
Componentsis the root container. Every feature is aComponentsubclass registered once and retrieved viacomponents.get(SomeComponent). Singletons perComponentsinstance.Worldsbundles scene + camera + renderer. A single app can have many worlds (e.g., main viewport + minimap). Each world has its own lifecycle.- Features are opt-in. Importing a component doesn't add cost until you
get()it. Most features are lazy-initialized on first access. - Events are typed.
new Event<Payload>()→.add(handler)/.trigger(payload). Used everywhere for loose coupling between components. - Front vs core split is strict. If a feature needs a renderer (postproduction, highlight materials, outlines), it lives in
packages/front/. If it only needs scene graph or data (classification, measurements, drawings metadata), it lives inpackages/core/. New features should default to core unless rendering is essential. - Fragments is the data layer.
@thatopen/componentsdoesn't own model data — it wraps@thatopen/fragments.FragmentsManagerholds theFragmentsModelsinstance; other components query it.
git clone https://github.com/ThatOpen/engine_components.git
cd engine_components
yarn install
yarn devyarn dev starts a Vite server that serves every example.ts in the repo. Browse to the printed URL and pick one. Changes to source files hot-reload.
Examples import from the local source (e.g., ../../../.. into packages/core/src/index.ts), so edits to either package show up immediately without rebuilding.
The only time you need an explicit build:
- Before opening a PR, run
yarn build-librariesto verify both packages build cleanly. This is the same check CI runs. - Testing consumer-side integration (i.e., using the built
dist/from another repo), runyarn build-coreand/oryarn build-front.
- Open an issue first (ask-first policy, per docs.thatopen.com/contributing).
- Decide core vs front: does it need a renderer? If yes, front. If no, core.
- Branch from
main. - Create a folder under the appropriate package (e.g.,
packages/core/src/core/MyFeature/) with:index.ts(re-exports)src/my-feature.ts(theComponentsubclass)example.ts+example.html(doubles as docs + regression test; deployed to the docs site)
- Register your component in the parent
index.tsbarrel export. - Update
CHANGELOG.mdunder## Unreleasedif user-visible. yarn build-librariesto verify the build.- Open a PR with a conventional-commits title (
feat:,fix:,feat!:for breaking).
- Reproduce in the relevant
example.ts, or add a new one underpackages/*/src/**/examples/if the existing tutorial doesn't exercise the broken path. - Keep the fix minimal. Add a regression path in the example where practical.
- PR title:
fix: <one-line description>.
- Don't bundle
threeorthree/...subpaths. Both vite configs externalizethreevia a function that matchesthreeand anythree/*subpath (not just the exact string). Addons likethree/examples/jsm/...andthree/webgpuinternally reach into three's build via relative paths — treating them as external prevents rollup from pulling the entire three source into the dist. If you're adding a new peer dep, match the pattern. Components.get()is a singleton registry. Calling it twice returns the same instance. Create-and-throw-away is not a supported pattern.World.camera/World.renderer/World.scenecan be reassigned. Don't cache references; re-read from the world on demand if you need them later.- The front package depends on core. Never import from
packages/front/insidepackages/core/. CI doesn't enforce this at build time, but it'll break consumers using only@thatopen/components. - Peer dependencies matter.
@thatopen/components-fronthas@thatopen/componentsas a peer. Your PR shouldn't bump the version range unless you genuinely depend on new API from core. - Most internal state lives on the
Componentinstance, not theComponentscontainer. When writing new components, prefer class fields to module-level globals so multipleComponentsinstances can coexist.
Read the existing examples under packages/*/src/**/examples/ — they cover most of the public API. Every feature has a dedicated example, and they're the fastest way to understand integration patterns.