@thatopen/services 0.3.14 → 0.3.16
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/dist/built-in/index.d.ts +165 -2
- package/dist/cli/commands/revit.d.ts +1 -1
- package/dist/cli/lib/revit-addin.d.ts +1 -1
- package/dist/cli.js +24 -13
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +46 -36
- package/docs/ai-quickstart.md +130 -130
- package/docs/builtin/paths.json +94 -86
- package/docs/cloud/GaussianSplatConverter.md +43 -43
- package/docs/cloud/PointCloudConverter.md +53 -53
- package/docs/revit-collab-quickstart.md +256 -205
- package/package.json +1 -1
- package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -0
- package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -160
- package/src/built-in/UIManager/src/models-list/example.ts +122 -122
- package/src/built-in/UIManager/src/topics-panel/example.ts +170 -0
- package/src/built-in/index.ts +1048 -881
- package/src/cli/templates/app/README.md +217 -217
- package/src/cli/templates/app/src/app.ts +37 -37
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
- package/src/cli/templates/app/src/main.ts +199 -199
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## Persisting project-scoped data with projectStorageContext
|
|
3
|
+
---
|
|
4
|
+
Most non-trivial apps built on top-app eventually need to persist some of
|
|
5
|
+
their own data inside the project — user preferences, cached results,
|
|
6
|
+
domain state that isn't a 3D model or a regular CDE file. The naive
|
|
7
|
+
approach is for each panel/manager to talk to the CDE client directly:
|
|
8
|
+
resolve or create a folder under `__project_data`, resolve or create the
|
|
9
|
+
file, decide between uploading a new file and versioning an existing one.
|
|
10
|
+
That's the same handful of steps every single consumer ends up
|
|
11
|
+
re-implementing.
|
|
12
|
+
|
|
13
|
+
top-app centralizes exactly that plumbing behind `projectStorageContext`, a
|
|
14
|
+
`@lit/context` value any descendant can `@consume`. It provides a small,
|
|
15
|
+
stable service object — not reactive state, since `save`/`flush` are
|
|
16
|
+
actions, not values to observe:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface ProjectStorage {
|
|
20
|
+
save(path: string, blob: Blob): void; // debounced (~2s), per path
|
|
21
|
+
flush(): Promise<void>; // cancel debounces, save everything NOW, await it
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`path` is resolved under `__project_data/`. Calling `save` schedules a
|
|
26
|
+
debounced upload — top-app creates whichever folders in the path don't
|
|
27
|
+
exist yet, creates the file the first time, and versions it (no explicit
|
|
28
|
+
`versionTag` needed — the backend assigns the next one) every time after.
|
|
29
|
+
Saves are debounced *per path*, so unrelated consumers saving around the
|
|
30
|
+
same time don't reset each other's timers, and a small floating indicator
|
|
31
|
+
(bottom-right, so it doesn't rely on the sidebar, which is optional and can
|
|
32
|
+
be hidden) reflects both phases distinctly: just the icon, no background/
|
|
33
|
+
border, while a save is debouncing (nothing to tell the user yet — it's
|
|
34
|
+
still waiting to see if more changes come in), and the full "Saving..."
|
|
35
|
+
pill once an upload is actually in flight. Each consumer only ever decides
|
|
36
|
+
*what* to save and *where* — never the CDE mechanics, nor the indicator.
|
|
37
|
+
|
|
38
|
+
Use `flush()` whenever you're about to do something that would conflict
|
|
39
|
+
with a save still in flight — e.g. triggering a cloud job that reads the
|
|
40
|
+
same file you just edited. It cancels any pending debounce, starts the
|
|
41
|
+
upload right away, and its returned promise resolves only once that
|
|
42
|
+
upload has actually finished.
|
|
43
|
+
|
|
44
|
+
By the end, you'll have a tiny "project notes" panel that autosaves free
|
|
45
|
+
text to `__project_data/notes/notes.json`, plus a "Save now" button that
|
|
46
|
+
demonstrates `flush()`.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { html, LitElement } from "lit";
|
|
50
|
+
import { customElement, state } from "lit/decorators.js";
|
|
51
|
+
import { consume } from "@lit/context";
|
|
52
|
+
import * as THREE from "three";
|
|
53
|
+
import * as OBC from "@thatopen/components";
|
|
54
|
+
import * as OBF from "@thatopen/components-front";
|
|
55
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
56
|
+
import * as BUI from "@thatopen/ui";
|
|
57
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
58
|
+
import type { App } from "../../index";
|
|
59
|
+
import { projectStorageContext, type ProjectStorage } from "../../src/context";
|
|
60
|
+
|
|
61
|
+
// The path is always relative to `__project_data/` — top-app creates
|
|
62
|
+
// whichever folders in it don't exist yet.
|
|
63
|
+
const NOTES_PATH = "notes/notes.json";
|
|
64
|
+
|
|
65
|
+
function notesBlob(text: string): Blob {
|
|
66
|
+
return new Blob([JSON.stringify({ text, savedAt: new Date().toISOString() })], {
|
|
67
|
+
type: "application/json",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@customElement("notes-panel")
|
|
72
|
+
class NotesPanel extends LitElement {
|
|
73
|
+
@consume({ context: projectStorageContext })
|
|
74
|
+
private _storage?: ProjectStorage;
|
|
75
|
+
|
|
76
|
+
@state() private _text = "";
|
|
77
|
+
@state() private _flushing = false;
|
|
78
|
+
|
|
79
|
+
render() {
|
|
80
|
+
return html`
|
|
81
|
+
<bim-panel label="Project Notes">
|
|
82
|
+
<bim-panel-section label="Notes" icon="tabler:notes">
|
|
83
|
+
<bim-text-input
|
|
84
|
+
vertical
|
|
85
|
+
type="area"
|
|
86
|
+
label="Note"
|
|
87
|
+
placeholder="Type something — autosaves as you go"
|
|
88
|
+
.value=${this._text}
|
|
89
|
+
@input=${(e: Event) => {
|
|
90
|
+
this._text = (e.target as HTMLInputElement).value ?? "";
|
|
91
|
+
// Debounced — top-app coalesces rapid edits into one upload.
|
|
92
|
+
this._storage?.save(NOTES_PATH, notesBlob(this._text));
|
|
93
|
+
}}
|
|
94
|
+
></bim-text-input>
|
|
95
|
+
<bim-button
|
|
96
|
+
label="Save now"
|
|
97
|
+
icon="tabler:cloud-upload"
|
|
98
|
+
.loading=${this._flushing}
|
|
99
|
+
@click=${async () => {
|
|
100
|
+
// Same blob, but flush() cancels the debounce and waits for
|
|
101
|
+
// the upload to actually finish before this resolves.
|
|
102
|
+
this._storage?.save(NOTES_PATH, notesBlob(this._text));
|
|
103
|
+
this._flushing = true;
|
|
104
|
+
await this._storage?.flush();
|
|
105
|
+
this._flushing = false;
|
|
106
|
+
}}
|
|
107
|
+
></bim-button>
|
|
108
|
+
</bim-panel-section>
|
|
109
|
+
</bim-panel>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- app wiring ----
|
|
115
|
+
|
|
116
|
+
const client = PlatformClient.fromPlatformContext();
|
|
117
|
+
const { components } = (await client.setup(
|
|
118
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
119
|
+
{ uuid: UIManager.uuid },
|
|
120
|
+
)) as { components: OBC.Components };
|
|
121
|
+
components.get(UIManager).init();
|
|
122
|
+
|
|
123
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
124
|
+
|
|
125
|
+
app.setup = (waitUntil) => {
|
|
126
|
+
waitUntil(
|
|
127
|
+
(async () => {
|
|
128
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
129
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
130
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
131
|
+
})(),
|
|
132
|
+
"Fragments Core",
|
|
133
|
+
);
|
|
134
|
+
return { components, client };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
app.elements = {
|
|
138
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
139
|
+
panel: () => html`<notes-panel></notes-panel>`,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
app.base = "viewer";
|
|
143
|
+
app.layouts = {
|
|
144
|
+
main: { label: "Main", icon: "solar:home-bold", template: `"panel" 1fr / 22rem` },
|
|
145
|
+
};
|
|
146
|
+
app.layout = "main";
|
|
147
|
+
|
|
148
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
149
|
+
container.appendChild(app);
|
|
150
|
+
document.body.style.margin = "0";
|
|
@@ -1,160 +1,183 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## clashes-panel
|
|
3
|
-
---
|
|
4
|
-
BIM coordinators need a dedicated review environment where they can see all
|
|
5
|
-
detected clashes, filter by status, navigate to each issue in the 3D viewer,
|
|
6
|
-
and mark items as resolved — without writing any UI code themselves.
|
|
7
|
-
|
|
8
|
-
`clashes-panel` is a ready-made web component that delivers exactly that. It
|
|
9
|
-
connects to `ClashesManager` automatically and manages the entire review
|
|
10
|
-
workflow: the detection matrix, the clash table with status filters, sphere
|
|
11
|
-
markers and element highlights in the viewer, run controls with live progress,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import
|
|
25
|
-
import * as
|
|
26
|
-
import * as
|
|
27
|
-
import * as
|
|
28
|
-
import * as
|
|
29
|
-
import
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
app.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
the
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
/* MD
|
|
2
|
+
## clashes-panel
|
|
3
|
+
---
|
|
4
|
+
BIM coordinators need a dedicated review environment where they can see all
|
|
5
|
+
detected clashes, filter by status, navigate to each issue in the 3D viewer,
|
|
6
|
+
and mark items as resolved — without writing any UI code themselves.
|
|
7
|
+
|
|
8
|
+
`clashes-panel` is a ready-made web component that delivers exactly that. It
|
|
9
|
+
connects to `ClashesManager` automatically and manages the entire review
|
|
10
|
+
workflow: the detection matrix, the clash table with status filters, sphere
|
|
11
|
+
markers and element highlights in the viewer, run controls with live progress,
|
|
12
|
+
a loading skeleton while fresh data reloads after a run completes, and
|
|
13
|
+
turning a cluster of clashes into a tracked BCF topic in one click.
|
|
14
|
+
|
|
15
|
+
This tutorial covers the prerequisites before mounting the panel; dropping it
|
|
16
|
+
into a `top-app` layout alongside a 3D viewer; and a detailed breakdown of
|
|
17
|
+
everything the panel handles automatically so you know what you do not need to
|
|
18
|
+
implement yourself.
|
|
19
|
+
|
|
20
|
+
By the end, you'll have a fully working clash-review environment running in
|
|
21
|
+
your application with a single line of markup.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { html } from "lit";
|
|
25
|
+
import * as THREE from "three";
|
|
26
|
+
import * as OBC from "@thatopen/components";
|
|
27
|
+
import * as OBF from "@thatopen/components-front";
|
|
28
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
29
|
+
import * as BUI from "@thatopen/ui";
|
|
30
|
+
import { PlatformClient, UIManager, ClashesManager, TopicsManager } from "@thatopen/services";
|
|
31
|
+
import type { App } from "../app/index";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/* MD
|
|
35
|
+
### ✅ Prerequisites
|
|
36
|
+
|
|
37
|
+
Three conditions must be met before the panel mounts:
|
|
38
|
+
|
|
39
|
+
1. **`UIManager` must be in the setup call.** It registers all platform web
|
|
40
|
+
components, including `<top-clashes-panel>` itself. Without it the element is
|
|
41
|
+
unknown to the browser and renders as an empty box.
|
|
42
|
+
|
|
43
|
+
2. **`ClashesManager.init(client)` must have resolved.** The panel reads all
|
|
44
|
+
its initial data from the manager in `connectedCallback`. If `init` has not
|
|
45
|
+
finished, the panel mounts into an empty state and never catches up because
|
|
46
|
+
it only subscribes to change events — it does not poll.
|
|
47
|
+
|
|
48
|
+
3. **`TopicsManager.init(client)` must have resolved too.** The panel's
|
|
49
|
+
"Create topic" button (turns a selection of clashes into a tracked BCF
|
|
50
|
+
topic) needs `Viewpoints.world` set to capture the camera, and `BCFTopics`
|
|
51
|
+
configured with `version: "3"` for the clash↔topic link to survive a
|
|
52
|
+
reload — `TopicsManager.init` is what sets both up. Skip it and the button
|
|
53
|
+
still works, but the topic it creates is subtly broken.
|
|
54
|
+
|
|
55
|
+
All three are naturally satisfied when you call both `init`s before appending
|
|
56
|
+
`top-app` to the document, which is the pattern shown below.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
const client = PlatformClient.fromPlatformContext();
|
|
60
|
+
|
|
61
|
+
const { components } = (await client.setup(
|
|
62
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
63
|
+
{ uuid: UIManager.uuid },
|
|
64
|
+
{ uuid: ClashesManager.uuid },
|
|
65
|
+
{ uuid: TopicsManager.uuid },
|
|
66
|
+
)) as { components: OBC.Components };
|
|
67
|
+
|
|
68
|
+
components.get(UIManager).init();
|
|
69
|
+
|
|
70
|
+
// Both inits must resolve before top-app (and therefore clashes-panel) mounts.
|
|
71
|
+
// Independent of each other — safe to run in parallel.
|
|
72
|
+
await Promise.all([
|
|
73
|
+
components.get(ClashesManager).init(client),
|
|
74
|
+
components.get(TopicsManager).init(client),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/* MD
|
|
78
|
+
### 🖥️ Wiring the panel
|
|
79
|
+
|
|
80
|
+
Add `clashes-panel` as a named area in `app.elements` and reference it in your
|
|
81
|
+
layout template. A side-by-side layout with the 3D viewer is the most natural
|
|
82
|
+
arrangement — the panel controls markers and highlights directly in the scene.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
86
|
+
|
|
87
|
+
app.setup = (waitUntil) => {
|
|
88
|
+
waitUntil(
|
|
89
|
+
(async () => {
|
|
90
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
91
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
92
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
93
|
+
})(),
|
|
94
|
+
"Fragments Core",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
waitUntil(
|
|
98
|
+
(async () => {
|
|
99
|
+
const loader = components.get(OBC.IfcLoader);
|
|
100
|
+
await loader.setup();
|
|
101
|
+
})(),
|
|
102
|
+
"IFC Loader",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return { components, client };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
app.elements = {
|
|
109
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
110
|
+
clashes: () => html`<top-clashes-panel></top-clashes-panel>`,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
app.layouts = {
|
|
114
|
+
main: {
|
|
115
|
+
label: "Main",
|
|
116
|
+
icon: "solar:3d-square-bold",
|
|
117
|
+
template: `"viewer clashes" 1fr / 1fr 22rem`,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
app.layout = "main";
|
|
122
|
+
|
|
123
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
124
|
+
container.appendChild(app);
|
|
125
|
+
document.body.style.margin = "0";
|
|
126
|
+
|
|
127
|
+
/* MD
|
|
128
|
+
### 🤖 What the panel manages automatically
|
|
129
|
+
|
|
130
|
+
Once mounted, `clashes-panel` takes ownership of the following without any
|
|
131
|
+
further code from you:
|
|
132
|
+
|
|
133
|
+
**Data synchronization** — the panel subscribes to every `ClashesManager`
|
|
134
|
+
event (`onJobsChanged`, `onRunsChanged`, `onClashesChanged`,
|
|
135
|
+
`onClashStatusChanged`, `onQueriesChanged`, `onRunningJobsChanged`) and keeps
|
|
136
|
+
its tables in sync. Changes made programmatically through the manager API are
|
|
137
|
+
reflected in the panel immediately.
|
|
138
|
+
|
|
139
|
+
**Run lifecycle** — a progress callout appears as soon as a run starts
|
|
140
|
+
(`onRunProgress`), updates its percentage and message in real time, and
|
|
141
|
+
disappears when the run completes or errors. If a run was already in progress
|
|
142
|
+
when the page loaded, the panel calls `reconnectIfRunning` automatically when
|
|
143
|
+
the user opens that job.
|
|
144
|
+
|
|
145
|
+
**Post-run reload skeleton** — after a successful run the manager reloads clash
|
|
146
|
+
data from storage. During that window (`onReloadingChanged = true`) the panel
|
|
147
|
+
replaces its entire content with an animated skeleton so the user is never left
|
|
148
|
+
looking at stale results that appear final.
|
|
149
|
+
|
|
150
|
+
**Sphere markers** — entering a job view places colored spheres in the 3D scene
|
|
151
|
+
at each clash location, transformed from IFC world space to viewer space.
|
|
152
|
+
Selecting a clash in the table zooms the camera, ghosts unrelated elements, and
|
|
153
|
+
re-focuses the markers on the selection. Leaving the job view removes all
|
|
154
|
+
markers and restores the full scene.
|
|
155
|
+
|
|
156
|
+
**Element highlights** — element A is highlighted green and element B red for
|
|
157
|
+
every new or active clash in the current job. Selecting a single clash refines
|
|
158
|
+
the highlight to that pair only. Clearing the selection or changing status
|
|
159
|
+
restores the full job highlight.
|
|
160
|
+
|
|
161
|
+
**Model awareness** — the panel tracks which fragment models are currently
|
|
162
|
+
loaded in the scene. Markers and highlights are activated only when the relevant
|
|
163
|
+
models are present, and are cleaned up automatically if a model is disposed
|
|
164
|
+
while the panel is open.
|
|
165
|
+
|
|
166
|
+
**Create topic from clash** — selecting one or more clashes and clicking
|
|
167
|
+
"Create topic" builds a BCF topic with an auto-generated title/description
|
|
168
|
+
summarizing the pair(s) and a viewpoint capturing the same selection and
|
|
169
|
+
red/green coloring shown in the table. The link back to the source clash(es)
|
|
170
|
+
is recorded separately so it survives independently of the topic's own BCF
|
|
171
|
+
fields. The button is disabled while the camera is still flying to a
|
|
172
|
+
selection or no models are loaded yet, so it can't fire on an empty scene.
|
|
173
|
+
|
|
174
|
+
**Save indicator** — every persisted change (clashes, queries, matrices,
|
|
175
|
+
topics created from a selection) routes through `top-app`'s shared
|
|
176
|
+
`projectStorageContext`, the same one `topics-panel` uses. A small indicator
|
|
177
|
+
reflects the real debounce-then-upload cycle: just an icon while a save is
|
|
178
|
+
pending, the full "Saving..." pill once the upload is actually in flight.
|
|
179
|
+
|
|
180
|
+
**Disconnection cleanup** — when the panel is removed from the DOM it calls
|
|
181
|
+
`hideMarkers` and `clearClashHighlight` on the manager, leaving the 3D scene
|
|
182
|
+
clean regardless of what was selected at the time.
|
|
183
|
+
*/
|