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_fragment.
Fragments is the BIM visualization and persistence layer: a compact file format (.frag, a FlatBuffers binary) plus the runtime that loads, renders, edits, and queries it. The runtime splits across the main thread and a Web Worker, with the worker owning the model data and streaming tiles to the main thread for rendering.
packages/fragments/src/
├── FragmentsModels/
│ ├── index.ts # FragmentsModels: entry point (load, dispose, update)
│ ├── src/
│ │ ├── model/ # FragmentsModel (main thread): raycast, highlight, visibility, edit wrappers
│ │ ├── multithreading/ # Main↔worker messaging (Connection, ThreadController)
│ │ ├── virtual-model/ # Worker-side state: VirtualFragmentsModel, tiles, boxes, materials
│ │ ├── edit/ # Editor + EditHelper: delta models, save/reset/undo/redo
│ │ └── single-threading/ # SingleThreadedFragmentsModel: Node.js / worker-less read+edit
│ ├── examples/ # Runnable tutorials (one folder per feature)
│ └── example.ts # Standalone dev entry point
├── Importers/
│ └── IfcImporter/ # IFC → .frag conversion
│ ├── index.ts # IfcImporter: classes to include, relations to index, settings
│ └── src/ # geometry readers (civil, grids, shells), property processor
├── Schema/ # FlatBuffers-generated TS (don't edit by hand; see `yarn fb`)
├── Utils/ # Shared helpers: edit-types, ifc-relations-map, shells, splitter
│ └── ifc-splitter/ # Stream-split a large IFC into smaller files
└── GeometryEngine/ # Primitive geometry (H-profiles, extrusions)
These four terms appear constantly in the codebase and mean very specific things. Confusing them is the single biggest source of wasted time when working on the worker-side code.
| Term | What it is | Where |
|---|---|---|
| localId | BIM identifier (e.g., IFC expressID). User-facing. May be sparse. |
Model.local_ids: [uint] |
| itemId | Dense index (0..N) into the parallel arrays meshes_items, global_transforms, material_ids, representation_ids, etc. One per unique BIM object. |
Array position, not a stored field |
| sampleId | Index into Meshes.samples[]. A render instance carrying item + material + representation + local_transform. Multiple samples can reference the same item. |
Meshes.samples[] |
| representationId | Index into Meshes.representations[]. The geometry shape (SHELL, CIRCLE_EXTRUSION). |
Meshes.representations[] |
Key conversions — do NOT rebuild these, they already exist:
- itemId → localId:
local_ids[meshes_items[itemId]].meshes_itemsis literally an itemId-indexed array of localIdIndices. Seeraycast-controller.ts'slocalIdsFromItemIdsfor the canonical two-lookup pattern. - sampleId → itemId:
samples[sampleId].item. - sampleId → representationId:
samples[sampleId].representation.
Before adding any Map<number, number> or Uint32Array inverse index inside the worker, check whether the flatbuffer already encodes what you need. Most of the time it does.
.fragfiles are FlatBuffers. The schema lives atpackages/fragments/flatbuffers/index.fbsand is generated intosrc/Schema/. Runyarn fbafter changing it.- Main thread (
src/FragmentsModels/src/model/) exposes the public API (model.raycast(),model.highlight(), etc.) and forwards most calls to the worker. - Worker (
src/FragmentsModels/src/virtual-model/+multithreading/) holds the flatbuffer, builds tiles, runs raycasts, serves queries. Messages flow asmodel.threads.invoke(modelId, "methodName", args). - Edits produce a delta model.
editor.edit()generates a separate.fragdelta buffer that's loaded as its ownFragmentsModelalongside the base. Reads must consider both (raycast, highlight, etc.).editor.save()flattens delta into a new committed buffer and reloads. - The IfcImporter is separable. It runs independently of the runtime and can be used in Node (
node-example.ts). It produces.fragbytes, nothing else.
git clone https://github.com/ThatOpen/engine_fragment.git
cd engine_fragment
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 fragments from ../../../index (source), so edits to packages/fragments/src/ show up immediately without rebuilding.
Two exceptions that need explicit builds:
- FlatBuffers schema changes (
flatbuffers/index.fbs) — runyarn fbto regeneratesrc/Schema/. - Worker changes — the worker is bundled separately. If you change anything under
FragmentsModels/src/virtual-model/ormultithreading/, and your example points at the pre-built worker URL, you need:Most examples in this repo point the worker at the local source (yarn workspace @thatopen/fragments build
"./src/multithreading/fragments-thread.ts"or equivalent), which hot-reloads like the rest.
- Open an issue first (ask-first policy, per docs.thatopen.com/contributing).
- Branch from
main. - Code + add or update an example under
src/FragmentsModels/examples/<feature>/with anexample.tsandexample.html. Examples double as docs (they're deployed to the docs site) and as regression tests. - Update
CHANGELOG.mdunder an## Unreleasedheading if the change is user-visible. Mark breaking changes explicitly with### ⚠ BREAKING CHANGES. yarn build-coreto verify the library still builds.- Open a PR with a conventional-commits title (
feat:,fix:,feat!:for breaking, etc.).
- Try to reproduce against
src/Importers/IfcImporter/test.tsorsrc/FragmentsModels/test.tsfirst — both are gitignored sandboxes meant for ad-hoc experiments. If you need an IFC or.fragto reproduce, drop it inresources/and reference it by relative URL. - If reproduction needs new library behavior, write the minimal patch + a test case in
example.ts(or a dedicated example if the feature warrants one). - PR title:
fix: <one-line description>.
- Delta models are separate
FragmentsModelentries infragments.models.list. Raycasting or highlighting must iterate the list, not just call on the base model, or edited items will be missed. getItemsWithGeometry()returnsItemobjects, notlocalIdnumbers. These have a back-reference to theFragmentsModeland can't be passed throughpostMessage. Callitem.getLocalId()first.representationFromGeometry()expects aUint32Arrayindex.THREE.BoxGeometryand friends useUint16Arrayby default; cast explicitly or normals/indices will be misread.- Edit request
tempIds only live within a singleeditor.edit()batch. Split across two calls and the second can't reference entities created in the first. CREATE_SAMPLE.data.itemis the global transform's tempId, not the item's. Common footgun.- Existing
.fragfiles don't auto-migrate. If you change what gets serialized (keys, categories, relation names), document that re-import is required.
Read the existing examples under src/FragmentsModels/examples/ — they cover most of the public API and use patterns. For internal mechanics, the test files (test.ts) show low-level usage of APIs that aren't part of the public surface.