@thatopen/services 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +76 -0
- package/CONTEXT.md +4 -4
- package/README.md +4 -4
- package/dist/cli.js +7 -5
- package/dist/core/client.d.ts +1 -1
- package/docs/access-backend-data.md +19 -0
- package/docs/app-architecture.md +94 -0
- package/docs/app-layout.md +139 -0
- package/docs/app-wiring.md +123 -0
- package/docs/bim-components/coding-conventions.md +128 -0
- package/docs/bim-components/element-collections.md +69 -0
- package/docs/bim-components/expose-events.md +84 -0
- package/docs/bim-components/library-examples.md +28 -0
- package/docs/bim-components/observable-collections.md +83 -0
- package/docs/bim-components/overview.md +160 -0
- package/docs/bim-components/per-frame-updates.md +20 -0
- package/docs/bim-components/save-and-restore-state.md +21 -0
- package/docs/bim-components/setup-and-cleanup.md +64 -0
- package/docs/bim-components/type-conventions.md +49 -0
- package/docs/bim-components/user-driven-object-creation.md +56 -0
- package/docs/cli-setup.md +54 -0
- package/docs/cloud-components.md +74 -0
- package/docs/connect-logic-to-ui.md +113 -0
- package/docs/previewing.md +45 -0
- package/docs/publishing.md +35 -0
- package/docs/scaffolding.md +54 -0
- package/docs/ui-components/async-actions.md +18 -0
- package/docs/ui-components/confirmation-dialog.md +39 -0
- package/docs/ui-components/data-table.md +185 -0
- package/docs/ui-components/display-text.md +39 -0
- package/docs/ui-components/inline-form.md +44 -0
- package/docs/ui-components/library-examples.md +24 -0
- package/docs/ui-components/overview.md +194 -0
- package/docs/ui-components/rendering-patterns.md +129 -0
- package/docs/ui-components/sections-layout.md +123 -0
- package/docs/update-grid-elements.md +46 -0
- package/docs/using-colors.md +32 -0
- package/docs/using-icons.md +37 -0
- package/package.json +3 -1
- package/src/cli/templates/bim/CONTEXT.md +3 -3
- package/src/cli/templates/bim/package.json +1 -1
- package/src/cli/templates/bim/src/app.ts +1 -1
- package/src/cli/templates/bim/src/bim-components/CloudRunner/index.ts +1 -1
- package/src/cli/templates/bim/src/main.ts +1 -1
- package/src/cli/templates/bim/src/setups/ui-manager.ts +1 -1
- package/src/cli/templates/bim/src/setups/viewports-manager.ts +1 -1
- package/src/cli/templates/cloud/CONTEXT.md +4 -4
- package/src/cli/templates/cloud/package.json +1 -1
- package/src/cli/templates/cloud/src/main.ts +1 -1
- package/src/cli/templates/cloud-test/package.json +1 -1
- package/src/cli/templates/cloud-test/src/main.ts +1 -1
- package/src/cli/templates/default/CONTEXT.md +4 -4
- package/src/cli/templates/default/package.json +3 -0
- package/src/cli/templates/default/src/main.ts +3 -3
- package/src/cli/templates/shared/AGENTS.md +23 -0
- package/src/cli/templates/shared/CLAUDE.md +1 -0
- package/src/cli/templates/shared/cloud/vite.config.js +3 -3
- package/src/cli/templates/test/package.json +1 -1
- package/src/cli/templates/test/src/main.ts +2 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Coding Conventions
|
|
2
|
+
|
|
3
|
+
General coding conventions that apply across all BIM components.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## No shorthand property declaration in constructors
|
|
8
|
+
|
|
9
|
+
Declare properties explicitly on the class body. Never use TypeScript's constructor shorthand (`public`, `private`, `readonly` parameter modifiers):
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// ✗ Avoid
|
|
13
|
+
constructor(public position: THREE.Vector3) {}
|
|
14
|
+
|
|
15
|
+
// ✓ Correct
|
|
16
|
+
position: THREE.Vector3;
|
|
17
|
+
|
|
18
|
+
constructor(position: THREE.Vector3) {
|
|
19
|
+
this.position = position;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Private member naming
|
|
26
|
+
|
|
27
|
+
Private **properties** use an underscore prefix. Private **methods** do not:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// Properties — underscore prefix
|
|
31
|
+
private _enabled = true;
|
|
32
|
+
private _mesh: THREE.Mesh | null = null;
|
|
33
|
+
|
|
34
|
+
// Methods — no prefix
|
|
35
|
+
private setupEvents() { ... }
|
|
36
|
+
private calculateResult() { ... }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Backing fields with getter/setter
|
|
42
|
+
|
|
43
|
+
When a property needs to trigger side effects on assignment — firing an event, propagating a value to Three.js objects — use a private backing field with a public getter/setter:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
private _visible = true;
|
|
47
|
+
|
|
48
|
+
get visible() {
|
|
49
|
+
return this._visible;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set visible(value: boolean) {
|
|
53
|
+
this._visible = value;
|
|
54
|
+
for (const mesh of this._meshes) {
|
|
55
|
+
mesh.visible = value;
|
|
56
|
+
}
|
|
57
|
+
this.onStateChanged.trigger(["visible"]);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Throw in late-initialization getters
|
|
64
|
+
|
|
65
|
+
When a property only exists after `setup()` or another explicit initialization step, the getter throws a descriptive error rather than returning `undefined`. This makes the missing initialization obvious at the call site:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
private _data: Data | null = null;
|
|
69
|
+
|
|
70
|
+
get data() {
|
|
71
|
+
if (!this._data) {
|
|
72
|
+
throw new Error("ActivityTracker: call setup() before accessing data.");
|
|
73
|
+
}
|
|
74
|
+
return this._data;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## `interface` vs `type`
|
|
81
|
+
|
|
82
|
+
Use `interface` for object shapes that may be extended or implemented by other classes. Use `type` for everything else — unions, primitive aliases, computed types, and tuples:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// ✓ interface — extendable object shape
|
|
86
|
+
export interface ActivityConfig {
|
|
87
|
+
color: THREE.Color;
|
|
88
|
+
autoColorize: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ✓ type — union
|
|
92
|
+
export type ActivityStatus = "notStarted" | "inProgress" | "completed";
|
|
93
|
+
|
|
94
|
+
// ✓ type — primitive alias
|
|
95
|
+
export type ActivityId = string;
|
|
96
|
+
|
|
97
|
+
// ✓ type — tuple / computed
|
|
98
|
+
export type SerializedMap = [string, number[]][];
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Prefer `DataMap` and `DataSet` over native `Map` and `Set`
|
|
104
|
+
|
|
105
|
+
When storing collections inside a component, always use `FRAGS.DataMap` and `FRAGS.DataSet` instead of the native `Map` and `Set`. They have an identical API but additionally emit lifecycle events (`onItemSet`, `onBeforeDelete`, `onItemDeleted`) that other parts of the app can react to.
|
|
106
|
+
|
|
107
|
+
See [`./observable-collections.md`](./observable-collections.md) for usage patterns.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## `async/await` over `.then()/.catch()`
|
|
112
|
+
|
|
113
|
+
Always prefer `async/await` for asynchronous code. It is more readable and consistent with the rest of the codebase:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// ✗ Avoid
|
|
117
|
+
this.loadModel(buffer).then((model) => {
|
|
118
|
+
this.process(model);
|
|
119
|
+
}).catch((err) => {
|
|
120
|
+
console.error(err);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ✓ Correct
|
|
124
|
+
async loadAndProcess(buffer: ArrayBuffer) {
|
|
125
|
+
const model = await this.loadModel(buffer);
|
|
126
|
+
await this.process(model);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Element Collections
|
|
2
|
+
|
|
3
|
+
`OBC.ModelIdMap` is the only acceptable way to reference a collection of elements in a `FragmentsModel`. Any method that operates on items — highlighting, hiding, filtering, calculating — takes or returns a `ModelIdMap`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `OBC.ModelIdMap`
|
|
8
|
+
|
|
9
|
+
Maps a model ID to a `Set` of local IDs:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// Shape: { [modelId: string]: Set<number> }
|
|
13
|
+
const items: OBC.ModelIdMap = {
|
|
14
|
+
"model-a": new Set([1, 2, 3]),
|
|
15
|
+
"model-b": new Set([7, 8]),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Iterating
|
|
19
|
+
for (const [modelId, localIds] of Object.entries(items)) {
|
|
20
|
+
const model = fragments.list.get(modelId);
|
|
21
|
+
for (const localId of localIds) { ... }
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## `OBC.ModelIdDataMap<T>`
|
|
28
|
+
|
|
29
|
+
Associates data of type `T` per item — a `DataMap<string, DataMap<number, T>>`. Used when a component stores per-item state:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const progress: OBC.ModelIdDataMap<{ startDate: Date }> = new FRAGS.DataMap();
|
|
33
|
+
|
|
34
|
+
// Write
|
|
35
|
+
let byLocalId = progress.get("model-a");
|
|
36
|
+
if (!byLocalId) {
|
|
37
|
+
byLocalId = new FRAGS.DataMap();
|
|
38
|
+
progress.set("model-a", byLocalId);
|
|
39
|
+
}
|
|
40
|
+
byLocalId.set(42, { startDate: new Date() });
|
|
41
|
+
|
|
42
|
+
// Read
|
|
43
|
+
const itemProgress = progress.get("model-a")?.get(42);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## `OBC.ModelIdMapUtils`
|
|
49
|
+
|
|
50
|
+
Static utility class for operating on `ModelIdMap` collections. Use it to combine, filter, compare, and serialize selections rather than implementing those operations manually:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import * as OBC from "@thatopen/components"
|
|
54
|
+
|
|
55
|
+
// Combine two selections
|
|
56
|
+
const combined = OBC.ModelIdMapUtils.join([selectionA, selectionB]);
|
|
57
|
+
|
|
58
|
+
// Remove deselected items from current selection
|
|
59
|
+
OBC.ModelIdMapUtils.remove(current, deselected);
|
|
60
|
+
|
|
61
|
+
// Guard before processing
|
|
62
|
+
if (OBC.ModelIdMapUtils.isEmpty(items)) return;
|
|
63
|
+
|
|
64
|
+
// Serialize for storage (Set<number> → number[])
|
|
65
|
+
const raw = OBC.ModelIdMapUtils.toRaw(items);
|
|
66
|
+
|
|
67
|
+
// Restore from storage
|
|
68
|
+
const restored = OBC.ModelIdMapUtils.fromRaw(raw);
|
|
69
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Expose Events
|
|
2
|
+
|
|
3
|
+
## `OBC.Event<T>`
|
|
4
|
+
|
|
5
|
+
Events are declared as `readonly` properties on the class. The type parameter `T` defines the payload — use `undefined` when there's nothing to pass.
|
|
6
|
+
|
|
7
|
+
Payload types are defined in `src/types.ts`, not inline, following the `{EventName}Payload` convention:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/types.ts
|
|
11
|
+
export type OperationCompletedPayload = { result: CalculationResult }
|
|
12
|
+
|
|
13
|
+
// index.ts
|
|
14
|
+
readonly onOperationCompleted = new OBC.Event<OperationCompletedPayload>();
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Triggering** — always after the fact, once the operation or change is done:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
this.onOperationCompleted.trigger({ result });
|
|
21
|
+
this.onDisposed.trigger(undefined);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Subscribing** — done externally by consumers:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const tracker = components.get(ActivityTracker);
|
|
28
|
+
tracker.onOperationCompleted.add(({ result }) => { ... });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## `onStateChanged` pattern
|
|
34
|
+
|
|
35
|
+
When a component has several reactive properties, a single `onStateChanged` event with a union payload is cleaner than one event per property:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
readonly onStateChanged = new OBC.Event<("enabled" | "color" | "mode")[]>();
|
|
39
|
+
|
|
40
|
+
private _enabled = true;
|
|
41
|
+
|
|
42
|
+
get enabled() {
|
|
43
|
+
return this._enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set enabled(value: boolean) {
|
|
47
|
+
this._enabled = value;
|
|
48
|
+
this.onStateChanged.trigger(["enabled"]);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Consumers can inspect the payload to react only to the properties they care about:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
tracker.onStateChanged.add((changed) => {
|
|
56
|
+
if (changed.includes("color")) updateColorUI();
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## `OBC.Eventable` and `EventManager`
|
|
63
|
+
|
|
64
|
+
Any component that exposes events must implement `OBC.Eventable` and use `EventManager`. Register every event in the constructor and call `events.reset()` in `dispose()`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export class ActivityTracker extends OBC.Component implements OBC.Eventable {
|
|
68
|
+
readonly onDisposed = new OBC.Event<undefined>();
|
|
69
|
+
readonly onStateChanged = new OBC.Event<string[]>();
|
|
70
|
+
|
|
71
|
+
readonly events = new OBC.EventManager();
|
|
72
|
+
|
|
73
|
+
constructor(components: OBC.Components) {
|
|
74
|
+
super(components);
|
|
75
|
+
components.add(ActivityTracker.uuid, this);
|
|
76
|
+
this.events.list.add(this.onDisposed);
|
|
77
|
+
this.events.list.add(this.onStateChanged);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
dispose() {
|
|
81
|
+
this.events.reset();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Library Examples
|
|
2
|
+
|
|
3
|
+
Before writing any implementation that uses That Open Engine — whether you know the component name or are trying to figure out how to achieve something — check whether an official example covers it. Official examples are the best starting point: they show correct initialization sequences, idiomatic API usage, and working patterns from the source authors.
|
|
4
|
+
|
|
5
|
+
## How to find an example
|
|
6
|
+
|
|
7
|
+
1. Fetch the `paths.json` for each repo below (you can do all three in parallel). Do not summarize the response — retain the full JSON in context and use the descriptions to reason about which examples are relevant.
|
|
8
|
+
2. Use the descriptions in the JSON to reason about which examples best match the user's intent. Prefer semantic matching over path name matching — a description may cover what the user needs even if the component name doesn't match exactly.
|
|
9
|
+
3. Only proceed to fetch an example if its description confirms that it directly covers the user's intent, or that it can serve as a building block for composing a new custom component. Do not fetch speculatively.
|
|
10
|
+
4. Construct the full URL: `{base_url}{path_entry}` and fetch the example file to use as your implementation reference.
|
|
11
|
+
|
|
12
|
+
Path names are descriptive — `VisibilityOperations`, `EditElements`, `SteelDetailing` — so matching by intent works well even when the user hasn't named a specific component.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Repositories
|
|
17
|
+
|
|
18
|
+
### engine_components
|
|
19
|
+
Components from `@thatopen/components` (OBC) and `@thatopen/components-front` (OBF).
|
|
20
|
+
|
|
21
|
+
- **paths.json**: `https://raw.githubusercontent.com/ThatOpen/engine_components/refs/heads/main/examples/paths.json`
|
|
22
|
+
- **Base URL**: `https://raw.githubusercontent.com/ThatOpen/engine_components/refs/heads/main/`
|
|
23
|
+
|
|
24
|
+
### engine_fragment
|
|
25
|
+
The `@thatopen/fragments` low-level library. There are no named OBC components here — users typically describe what they want to do (e.g., "edit model properties", "modify elements", "work with visibility").
|
|
26
|
+
|
|
27
|
+
- **paths.json**: `https://raw.githubusercontent.com/ThatOpen/engine_fragment/refs/heads/main/examples/paths.json`
|
|
28
|
+
- **Base URL**: `https://raw.githubusercontent.com/ThatOpen/engine_fragment/refs/heads/main/`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Observable Collections
|
|
2
|
+
|
|
3
|
+
`FRAGS.DataMap` and `FRAGS.DataSet` are lifecycle-aware extensions of `Map` and `Set`. The only difference from their native counterparts is that they emit events when items are added or removed, making the collection observable.
|
|
4
|
+
|
|
5
|
+
Use `DataMap` when there is a clear key to identify each item. Use `DataSet` when the item itself is the identifier. `DataMap` is the default choice in most cases.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## `FRAGS.DataMap<K, V>`
|
|
10
|
+
|
|
11
|
+
### Declaration
|
|
12
|
+
|
|
13
|
+
The primary collection of a component is conventionally named `list`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import * as FRAGS from "@thatopen/fragments"
|
|
17
|
+
|
|
18
|
+
readonly list = new FRAGS.DataMap<string, SpotElevation>();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Reactive events
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
this.list.onItemSet.add(({ key, value }) => {
|
|
25
|
+
// called after an item is added
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.list.onBeforeDelete.add(({ key, value }) => {
|
|
29
|
+
// called before an item is removed — use to clean up resources
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.list.onItemDeleted.add(({ key }) => {
|
|
33
|
+
// called after an item is removed
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Get-or-create
|
|
38
|
+
|
|
39
|
+
When inserting into a `DataMap` where the entry may not exist yet, use a get-or-create pattern:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
let entry = this.list.get(key);
|
|
43
|
+
if (!entry) {
|
|
44
|
+
entry = new SpotElevation();
|
|
45
|
+
this.list.set(key, entry);
|
|
46
|
+
}
|
|
47
|
+
entry.doSomething();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Guard
|
|
51
|
+
|
|
52
|
+
A guard runs before every `set()` call and can block the insertion by returning `false`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
this.list.guard = (key, value) => {
|
|
56
|
+
return value.isValid;
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## `FRAGS.DataSet<T>`
|
|
63
|
+
|
|
64
|
+
Same API as `DataMap` but without keys — the item itself is the identifier:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
readonly list = new FRAGS.DataSet<SpotElevation>();
|
|
68
|
+
|
|
69
|
+
this.list.onItemSet.add(({ value }) => { ... });
|
|
70
|
+
this.list.onBeforeDelete.add(({ value }) => { ... });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Collections of auxiliary class instances
|
|
76
|
+
|
|
77
|
+
When a component manages instances of its own auxiliary classes, cleanup typically happens in `onBeforeDelete`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
this.list.onBeforeDelete.add(({ value: spotElevation }) => {
|
|
81
|
+
spotElevation.dispose();
|
|
82
|
+
});
|
|
83
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# BIM Component Creation
|
|
2
|
+
|
|
3
|
+
Architecture, patterns, and step-by-step guide for creating BIM components with That Open Engine (OBC/OBF) — the domain-logic layer that wraps fragments, IFC data, measurements, selections, or any non-UI behavior.
|
|
4
|
+
|
|
5
|
+
## Working mode
|
|
6
|
+
|
|
7
|
+
Before doing anything else, **read [`./library-examples.md`](./library-examples.md) and fetch the `paths.json` for both `engine_components` and `engine_fragment` in parallel.** Review all descriptions to understand what the engine already provides before forming any opinion about what needs to be built.
|
|
8
|
+
|
|
9
|
+
Only after that, **propose an implementation plan and wait for the user's approval.**
|
|
10
|
+
|
|
11
|
+
The proposal must describe:
|
|
12
|
+
- Whether the need is covered by an existing engine component (consume via `components.get()`)
|
|
13
|
+
- Whether existing components can be composed to cover it
|
|
14
|
+
- Only if neither applies: which new component(s) will be created, what properties, events, and public methods it will expose, and whether it needs lifecycle, serialization, render loop, or interactive creation interfaces
|
|
15
|
+
|
|
16
|
+
Only proceed with changes after the user explicitly confirms the plan. If the scope is unclear, ask first — do not assume.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What is a BIM Component?
|
|
21
|
+
|
|
22
|
+
That Open Engine is the collection of packages that power the app: `@thatopen/components`, `@thatopen/components-front`, `@thatopen/fragments`, `@thatopen/ui`, and `@thatopen/ui-obc`. BIM components are built on top of this ecosystem.
|
|
23
|
+
|
|
24
|
+
A BIM component is a class that extends `OBC.Component` and encapsulates domain logic — IFC queries, calculations, data management, fragment operations, etc. It exposes that logic through a clean, event-driven interface that UI templates and other components can consume.
|
|
25
|
+
|
|
26
|
+
### The engine works with Fragments
|
|
27
|
+
|
|
28
|
+
That Open Engine does not work with IFC STEP files directly. It works with **Fragments** — an open binary format (`.frag`) built for performance: a 2 GB IFC file becomes ~80 MB, loads in seconds at 60fps in the browser.
|
|
29
|
+
|
|
30
|
+
The runtime representation of a loaded model is a `FragmentsModel`. Its data schema usually mirrors the IFC SCHEMA (local IDs map to IFC express IDs, the spatial structure parallels IFC hierarchy, properties follow IFC attributes and relations), but it is not IFC STEP — it is Fragments. Any component that queries elements, reads properties, or operates on geometry works through the `FragmentsModel` API.
|
|
31
|
+
|
|
32
|
+
For loading models, querying elements, or reading properties, fetch the relevant example from [`./library-examples.md`](./library-examples.md) before writing any implementation code.
|
|
33
|
+
|
|
34
|
+
### When to create one
|
|
35
|
+
|
|
36
|
+
Create a BIM component when custom domain logic is needed.
|
|
37
|
+
|
|
38
|
+
After reviewing the `paths.json` descriptions fetched in the working mode step:
|
|
39
|
+
|
|
40
|
+
- If an existing component covers the need → consume it via `this.components.get()` and fetch its example. Do not reimplement.
|
|
41
|
+
- If existing components can be composed to cover it → compose them, fetching the relevant examples as building blocks.
|
|
42
|
+
- Only if the need is genuinely not covered → propose creating a custom component.
|
|
43
|
+
|
|
44
|
+
Custom components can themselves consume built-in components via `this.components.get()`, so examples remain useful even when building something new.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Step 1 — Define the component
|
|
49
|
+
|
|
50
|
+
Names should be short but descriptive. Use PascalCase for both folder and class, matching exactly:
|
|
51
|
+
|
|
52
|
+
| Artifact | Convention | Example |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Folder | PascalCase | `ActivityTracker/` |
|
|
55
|
+
| Class | PascalCase | `ActivityTracker` |
|
|
56
|
+
| Types prefix | PascalCase matching class | `ActivityTrackerConfig`, `ActivityTrackerResult` |
|
|
57
|
+
|
|
58
|
+
Use the `Manager` suffix when the component creates instances of something or governs a broad concern — consistent with `FragmentsManager`, `Classifier`, etc. For focused, single-responsibility components, omit it.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Step 2 — Create the file structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
ActivityTracker/
|
|
66
|
+
index.ts → class definition + re-exports ./src
|
|
67
|
+
src/
|
|
68
|
+
index.ts → re-exports types and support files
|
|
69
|
+
types.ts → interfaces, type aliases, enums
|
|
70
|
+
SpotElevation.ts → auxiliary class (if needed)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Only one class per component extends `OBC.Component` — the main one in `index.ts`. Any other classes the component defines or instantiates are plain TypeScript classes that live in `src/` alongside `types.ts`, and never extend `OBC.Component`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Step 3 — Implement the class in `index.ts`
|
|
78
|
+
|
|
79
|
+
### Skeleton
|
|
80
|
+
|
|
81
|
+
Every component needs a unique, hardcoded static UUID — a fixed string literal defined once and never changed. The constructor must call `super(components)` and register the instance with `components.add(UUID, this)` so it becomes globally retrievable via `components.get(ActivityTracker)`.
|
|
82
|
+
|
|
83
|
+
Before writing the implementation, read [`./coding-conventions.md`](./coding-conventions.md) for naming rules, backing fields, and async patterns that apply throughout the class.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import * as OBC from "@thatopen/components"
|
|
87
|
+
|
|
88
|
+
export class ActivityTracker extends OBC.Component {
|
|
89
|
+
static readonly uuid = "1b43361b-ef43-4207-9c40-dbed37bec0b6" as const;
|
|
90
|
+
|
|
91
|
+
enabled = true;
|
|
92
|
+
|
|
93
|
+
constructor(components: OBC.Components) {
|
|
94
|
+
super(components);
|
|
95
|
+
components.add(ActivityTracker.uuid, this);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export * from "./src"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Interfaces
|
|
103
|
+
|
|
104
|
+
OBC defines capability interfaces that components can implement to stay consistent with each other and with the engine. Pick the resource that matches your need:
|
|
105
|
+
|
|
106
|
+
| I need... | Resource |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Initialize the component with config, or clean up resources when it's destroyed | [`./setup-and-cleanup.md`](./setup-and-cleanup.md) |
|
|
109
|
+
| Save and restore state across sessions | [`./save-and-restore-state.md`](./save-and-restore-state.md) |
|
|
110
|
+
| Run logic on every frame of the render loop | [`./per-frame-updates.md`](./per-frame-updates.md) |
|
|
111
|
+
| Implement a flow where the user creates objects in the scene step by step | [`./user-driven-object-creation.md`](./user-driven-object-creation.md) |
|
|
112
|
+
| Expose events or react to state changes | [`./expose-events.md`](./expose-events.md) |
|
|
113
|
+
|
|
114
|
+
### Accessing other components
|
|
115
|
+
|
|
116
|
+
Resolve dependencies at call time — never store component instances as fields as it may create memory leaks:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const fragments = this.components.get(OBC.FragmentsManager);
|
|
120
|
+
const highlighter = this.components.get(OBF.Highlighter);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Step 4 — Define types in `src/types.ts`
|
|
126
|
+
|
|
127
|
+
Types are defined in parallel as the class needs them, not upfront. All typing for the component lives here — domain interfaces, event payloads, serialized types — so there is a single place to look.
|
|
128
|
+
|
|
129
|
+
See [`./type-conventions.md`](./type-conventions.md) for common patterns (runtime vs serialized types, generics, etc.).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Step 5 — Export from the barrel
|
|
134
|
+
|
|
135
|
+
Re-export the component from the project's barrel index so it's accessible from a single import point:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
export * from "./ActivityTracker"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Wiring up
|
|
144
|
+
|
|
145
|
+
Instantiating the component and connecting it to engine events is not the component's responsibility — that belongs in external initialization code. The constructor should never call other components. See [`../app-wiring.md`](../app-wiring.md) for how this is done.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## See also
|
|
150
|
+
|
|
151
|
+
- [`./library-examples.md`](./library-examples.md) — Find official usage examples and discover what the engine provides
|
|
152
|
+
- [`./element-collections.md`](./element-collections.md) — Represent or combine element collections across components
|
|
153
|
+
- [`./observable-collections.md`](./observable-collections.md) — Store component-internal data with reactive notifications
|
|
154
|
+
- [`./expose-events.md`](./expose-events.md) — Expose events for other components or the UI to react to
|
|
155
|
+
- [`./setup-and-cleanup.md`](./setup-and-cleanup.md) — Initialize the component with config or clean up resources on destroy
|
|
156
|
+
- [`./save-and-restore-state.md`](./save-and-restore-state.md) — Persist and restore state across sessions
|
|
157
|
+
- [`./per-frame-updates.md`](./per-frame-updates.md) — Run logic on every frame of the render loop
|
|
158
|
+
- [`./user-driven-object-creation.md`](./user-driven-object-creation.md) — Let the user create objects in the scene step by step
|
|
159
|
+
- [`./type-conventions.md`](./type-conventions.md) — Define component types (runtime vs serialized, generics)
|
|
160
|
+
- [`./coding-conventions.md`](./coding-conventions.md) — Code conventions (naming, backing fields, async)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Per-Frame Updates
|
|
2
|
+
|
|
3
|
+
## `OBC.Updateable`
|
|
4
|
+
|
|
5
|
+
Implement when the component needs to execute logic on every frame — animating objects, polling state, updating visual feedback.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
export class ActivityTracker extends OBC.Component implements OBC.Updateable {
|
|
9
|
+
readonly onBeforeUpdate = new OBC.Event<undefined>();
|
|
10
|
+
readonly onAfterUpdate = new OBC.Event<undefined>();
|
|
11
|
+
|
|
12
|
+
update(delta?: number) {
|
|
13
|
+
this.onBeforeUpdate.trigger(undefined);
|
|
14
|
+
// per-frame logic
|
|
15
|
+
this.onAfterUpdate.trigger(undefined);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The component is driven externally — a world or a setup file calls `update()` each frame. The component never drives itself.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Save and Restore State
|
|
2
|
+
|
|
3
|
+
## `OBC.Serializable<D, S>`
|
|
4
|
+
|
|
5
|
+
Implement when the component must persist its state — to a file, to the cloud, or across sessions.
|
|
6
|
+
|
|
7
|
+
`D` is the runtime type, `S` is the serialized (JSON-safe) equivalent. Both are defined in `src/types.ts`. See [`./type-conventions.md`](./type-conventions.md) for the serialization patterns, including the `localId` → GUID conversion that must happen before storing any item references.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export class ActivityTracker extends OBC.Component
|
|
11
|
+
implements OBC.Serializable<ActivityTrackerData, SerializedActivityTrackerData> {
|
|
12
|
+
|
|
13
|
+
export(): SerializedActivityTrackerData {
|
|
14
|
+
// convert runtime state to JSON-safe structure
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import(data: SerializedActivityTrackerData) {
|
|
18
|
+
// restore runtime state from serialized structure
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Setup and Cleanup
|
|
2
|
+
|
|
3
|
+
Two interfaces govern a component's lifecycle: `OBC.Configurable` for one-time initialization before use, and `OBC.Disposable` for cleanup when the component is destroyed.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `OBC.Configurable<TManager, TConfig>`
|
|
8
|
+
|
|
9
|
+
Implement when the component requires one-time initialization. This separates construction (cheap, no side effects) from setup (resolves dependencies, applies config, registers event listeners).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export interface ActivityTrackerConfig {
|
|
13
|
+
color: THREE.Color;
|
|
14
|
+
autoColorize: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ActivityTracker extends OBC.Component
|
|
18
|
+
implements OBC.Configurable<ActivityTrackerConfigManager, ActivityTrackerConfig> {
|
|
19
|
+
|
|
20
|
+
isSetup = false;
|
|
21
|
+
readonly onSetup = new OBC.Event<undefined>();
|
|
22
|
+
|
|
23
|
+
protected _defaultConfig: ActivityTrackerConfig = {
|
|
24
|
+
color: new THREE.Color("#6528d7"),
|
|
25
|
+
autoColorize: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
setup(config?: Partial<ActivityTrackerConfig>) {
|
|
29
|
+
if (this.isSetup) return;
|
|
30
|
+
const fullConfig = { ...this._defaultConfig, ...config };
|
|
31
|
+
// apply config, resolve dependencies...
|
|
32
|
+
this.isSetup = true;
|
|
33
|
+
this.onSetup.trigger(undefined);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The `if (this.isSetup) return` guard ensures setup only runs once. The constructor must never call other components — that belongs in `setup()`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## `OBC.Disposable`
|
|
43
|
+
|
|
44
|
+
Implement when the component holds resources that must be explicitly released — Three.js geometries and materials, event subscriptions to external components, workers.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export class ActivityTracker extends OBC.Component implements OBC.Disposable {
|
|
48
|
+
readonly onDisposed = new OBC.Event<undefined>();
|
|
49
|
+
|
|
50
|
+
dispose() {
|
|
51
|
+
// 1. Release Three.js resources
|
|
52
|
+
this._mesh?.geometry.dispose();
|
|
53
|
+
this._material.dispose();
|
|
54
|
+
|
|
55
|
+
// 2. Reset all events (if OBC.Eventable is also implemented)
|
|
56
|
+
this.events.reset();
|
|
57
|
+
|
|
58
|
+
// 3. Signal disposal — always last
|
|
59
|
+
this.onDisposed.trigger(undefined);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`onDisposed` must be triggered at the end of `dispose()`, after all cleanup is done.
|