@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
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## top-models-list
|
|
3
|
-
---
|
|
4
|
-
top-models-list is the project's 3D-viewable-files tree: it mirrors the real storage folder structure, converts IFC to fragments, and loads `.frag`/`.potree`/`.splat` models into the 3D scene. Its loaders are PLUGGABLE — the panel owns the UI and the orchestration (progress, refresh, association), but the HOST decides how each extra file format is loaded, so you can teach it new formats without forking the built-in.
|
|
5
|
-
|
|
6
|
-
This tutorial covers the two extension points. First, registering a custom loader for a file extension — either imperatively with `registerLoader(ext, fn)` or up front via the `loaders` property; a loader receives `(fileId, ctx)` where `ctx` carries the engine (`components`), the platform `client`, and per-file alignment persistence. Second, overriding the IFC to fragments `converter` — omit it to use the built-in default (which drives the project's IfcFragmenter cloud component, resolved by its stable name — see `adrs/resolve-ifc-fragmenter-by-stable-name.md`), or provide one to run conversion elsewhere.
|
|
7
|
-
|
|
8
|
-
A behaviour worth knowing: registering a loader does NOT control whether a file shows up in the tree — the tree always mirrors the project's real storage folders, so an unrecognized file is still listed as long as it's a source (IFC/LAS/PLY) or derivative (`.frag`/`.potree`/`.splat`) extension; the loaders registry only decides whether a file's row gets a load action or none.
|
|
9
|
-
|
|
10
|
-
Known gaps (intentionally deferred, not forgotten): file upload and the base-model/auto-coordinate settings aren't ported, so `allow-upload` is currently a no-op. Everything else per-file is here, including editable attach/detach via a picker — MINUS delete, dropped on purpose. See `models-list/legacy-example.ts` + `models-list/impl.ts` for the retired implementation those gaps would be ported from.
|
|
11
|
-
|
|
12
|
-
By the end you'll have a files tree that knows how to load a custom format and how to run a custom IFC conversion, with the panel's UI untouched.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { html } from "lit";
|
|
16
|
-
import * as THREE from "three";
|
|
17
|
-
import * as OBC from "@thatopen/components";
|
|
18
|
-
import * as OBF from "@thatopen/components-front";
|
|
19
|
-
import * as FRAGS from "@thatopen/fragments";
|
|
20
|
-
import * as BUI from "@thatopen/ui";
|
|
21
|
-
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
22
|
-
import type { App } from "../app/index";
|
|
23
|
-
import type { ModelsListV2 } from "./src/v2/models-list-panel";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const client = PlatformClient.fromPlatformContext();
|
|
27
|
-
|
|
28
|
-
// UIManager registers every platform web component (top-app, top-viewer,
|
|
29
|
-
// top-models-list, …) before the DOM renders.
|
|
30
|
-
const { components } = (await client.setup(
|
|
31
|
-
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
32
|
-
{ uuid: UIManager.uuid },
|
|
33
|
-
)) as { components: OBC.Components };
|
|
34
|
-
|
|
35
|
-
components.get(UIManager).init();
|
|
36
|
-
|
|
37
|
-
const app = document.createElement("top-app") as unknown as App;
|
|
38
|
-
|
|
39
|
-
app.setup = (waitUntil) => {
|
|
40
|
-
waitUntil(
|
|
41
|
-
(async () => {
|
|
42
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
43
|
-
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
44
|
-
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
45
|
-
classicWorker: true,
|
|
46
|
-
});
|
|
47
|
-
})(),
|
|
48
|
-
"Fragments Core",
|
|
49
|
-
);
|
|
50
|
-
return { components, client };
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// One stable <top-models-list>, kept by reference so re-rendering top-app
|
|
54
|
-
// reuses it. The loaders are read LIVE, so registration can happen at any time.
|
|
55
|
-
const modelsList = document.createElement("top-models-list") as ModelsListV2;
|
|
56
|
-
|
|
57
|
-
// ── Extension point 1: a custom loader, keyed by file extension ──────────────
|
|
58
|
-
// A ModelLoader is `(fileId, ctx) => void | Promise<void>`. The panel calls it
|
|
59
|
-
// when the user loads a file whose extension matches. `ctx` gives you everything
|
|
60
|
-
// you need: { name, ext, components, client, getAlignment, setAlignment }. Do
|
|
61
|
-
// whatever it takes to put the file in the scene — fetch it via the client,
|
|
62
|
-
// parse it, and add geometry to a world from `components`.
|
|
63
|
-
modelsList.registerLoader("xyz", async (fileId, ctx) => {
|
|
64
|
-
// const buffer = await ctx.client
|
|
65
|
-
// .downloadFile(fileId)
|
|
66
|
-
// .then((r: Response) => r.arrayBuffer());
|
|
67
|
-
// ...parse `buffer` and add it to the world here...
|
|
68
|
-
// Persist/restore a placement matrix across reloads via the panel's app-data:
|
|
69
|
-
// const saved = ctx.getAlignment(fileId);
|
|
70
|
-
// ctx.setAlignment(fileId, matrix.toArray());
|
|
71
|
-
console.log(`[xyz loader] would load "${ctx.name}" (${fileId})`, ctx.components);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Equivalent declarative form — hand the whole registry over at once:
|
|
75
|
-
// modelsList.loaders = { xyz: async (fileId, ctx) => { ... } };
|
|
76
|
-
//
|
|
77
|
-
// Registering a loader for an extension gives its files a load action here —
|
|
78
|
-
// but doesn't control visibility (see the note above). For a CONVERT-style
|
|
79
|
-
// action — e.g. a PLY that runs a PLY→3D-tiles cloud component then views the
|
|
80
|
-
// result — register a RICH entry so the button reads correctly:
|
|
81
|
-
// modelsList.registerLoader("ply", {
|
|
82
|
-
// label: "Generate 3D tiles",
|
|
83
|
-
// icon: "mdi:cube-scan",
|
|
84
|
-
// fn: async (fileId, ctx) => {
|
|
85
|
-
// // const { executionId } = await ctx.client.executeComponent(...);
|
|
86
|
-
// // ...poll until done, then open the produced tileset...
|
|
87
|
-
// },
|
|
88
|
-
// });
|
|
89
|
-
|
|
90
|
-
// ── Extension point 2: override the IFC → fragments converter ────────────────
|
|
91
|
-
// OMIT this to keep the built-in default (drives the project's IfcFragmenter
|
|
92
|
-
// cloud component via the platform client). Provide it to run conversion
|
|
93
|
-
// elsewhere / differently — the panel keeps the progress bar, reload-survival,
|
|
94
|
-
// association and refresh; your converter just `start`s and is `poll`ed.
|
|
95
|
-
modelsList.converter = {
|
|
96
|
-
async start(fileId) {
|
|
97
|
-
// kick off your conversion; return an execution id to poll
|
|
98
|
-
return `exec-${fileId}`;
|
|
99
|
-
},
|
|
100
|
-
async poll(executionId) {
|
|
101
|
-
// report progress; when done, hand back the produced .frag id
|
|
102
|
-
return { done: true, ok: true, fragId: "demo-frag", message: "Converted" };
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Mount the panel into a one-area layout. (In a real app it sits beside a
|
|
107
|
-
// top-viewer so loaded models have a world to render into.)
|
|
108
|
-
app.elements = {
|
|
109
|
-
files: () => html`${modelsList}`,
|
|
110
|
-
};
|
|
111
|
-
app.layouts = {
|
|
112
|
-
main: {
|
|
113
|
-
label: "Files",
|
|
114
|
-
icon: "mdi:folder-multiple-outline",
|
|
115
|
-
template: `"files" 1fr / 1fr`,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
app.layout = "main";
|
|
119
|
-
|
|
120
|
-
const container = document.getElementById("that-open-app") ?? document.body;
|
|
121
|
-
container.appendChild(app);
|
|
122
|
-
document.body.style.margin = "0";
|
|
1
|
+
/* MD
|
|
2
|
+
## top-models-list
|
|
3
|
+
---
|
|
4
|
+
top-models-list is the project's 3D-viewable-files tree: it mirrors the real storage folder structure, converts IFC to fragments, and loads `.frag`/`.potree`/`.splat` models into the 3D scene. Its loaders are PLUGGABLE — the panel owns the UI and the orchestration (progress, refresh, association), but the HOST decides how each extra file format is loaded, so you can teach it new formats without forking the built-in.
|
|
5
|
+
|
|
6
|
+
This tutorial covers the two extension points. First, registering a custom loader for a file extension — either imperatively with `registerLoader(ext, fn)` or up front via the `loaders` property; a loader receives `(fileId, ctx)` where `ctx` carries the engine (`components`), the platform `client`, and per-file alignment persistence. Second, overriding the IFC to fragments `converter` — omit it to use the built-in default (which drives the project's IfcFragmenter cloud component, resolved by its stable name — see `adrs/resolve-ifc-fragmenter-by-stable-name.md`), or provide one to run conversion elsewhere.
|
|
7
|
+
|
|
8
|
+
A behaviour worth knowing: registering a loader does NOT control whether a file shows up in the tree — the tree always mirrors the project's real storage folders, so an unrecognized file is still listed as long as it's a source (IFC/LAS/PLY) or derivative (`.frag`/`.potree`/`.splat`) extension; the loaders registry only decides whether a file's row gets a load action or none.
|
|
9
|
+
|
|
10
|
+
Known gaps (intentionally deferred, not forgotten): file upload and the base-model/auto-coordinate settings aren't ported, so `allow-upload` is currently a no-op. Everything else per-file is here, including editable attach/detach via a picker — MINUS delete, dropped on purpose. See `models-list/legacy-example.ts` + `models-list/impl.ts` for the retired implementation those gaps would be ported from.
|
|
11
|
+
|
|
12
|
+
By the end you'll have a files tree that knows how to load a custom format and how to run a custom IFC conversion, with the panel's UI untouched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { html } from "lit";
|
|
16
|
+
import * as THREE from "three";
|
|
17
|
+
import * as OBC from "@thatopen/components";
|
|
18
|
+
import * as OBF from "@thatopen/components-front";
|
|
19
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
20
|
+
import * as BUI from "@thatopen/ui";
|
|
21
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
22
|
+
import type { App } from "../app/index";
|
|
23
|
+
import type { ModelsListV2 } from "./src/v2/models-list-panel";
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const client = PlatformClient.fromPlatformContext();
|
|
27
|
+
|
|
28
|
+
// UIManager registers every platform web component (top-app, top-viewer,
|
|
29
|
+
// top-models-list, …) before the DOM renders.
|
|
30
|
+
const { components } = (await client.setup(
|
|
31
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
32
|
+
{ uuid: UIManager.uuid },
|
|
33
|
+
)) as { components: OBC.Components };
|
|
34
|
+
|
|
35
|
+
components.get(UIManager).init();
|
|
36
|
+
|
|
37
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
38
|
+
|
|
39
|
+
app.setup = (waitUntil) => {
|
|
40
|
+
waitUntil(
|
|
41
|
+
(async () => {
|
|
42
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
43
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
44
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
45
|
+
classicWorker: true,
|
|
46
|
+
});
|
|
47
|
+
})(),
|
|
48
|
+
"Fragments Core",
|
|
49
|
+
);
|
|
50
|
+
return { components, client };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// One stable <top-models-list>, kept by reference so re-rendering top-app
|
|
54
|
+
// reuses it. The loaders are read LIVE, so registration can happen at any time.
|
|
55
|
+
const modelsList = document.createElement("top-models-list") as ModelsListV2;
|
|
56
|
+
|
|
57
|
+
// ── Extension point 1: a custom loader, keyed by file extension ──────────────
|
|
58
|
+
// A ModelLoader is `(fileId, ctx) => void | Promise<void>`. The panel calls it
|
|
59
|
+
// when the user loads a file whose extension matches. `ctx` gives you everything
|
|
60
|
+
// you need: { name, ext, components, client, getAlignment, setAlignment }. Do
|
|
61
|
+
// whatever it takes to put the file in the scene — fetch it via the client,
|
|
62
|
+
// parse it, and add geometry to a world from `components`.
|
|
63
|
+
modelsList.registerLoader("xyz", async (fileId, ctx) => {
|
|
64
|
+
// const buffer = await ctx.client
|
|
65
|
+
// .downloadFile(fileId)
|
|
66
|
+
// .then((r: Response) => r.arrayBuffer());
|
|
67
|
+
// ...parse `buffer` and add it to the world here...
|
|
68
|
+
// Persist/restore a placement matrix across reloads via the panel's app-data:
|
|
69
|
+
// const saved = ctx.getAlignment(fileId);
|
|
70
|
+
// ctx.setAlignment(fileId, matrix.toArray());
|
|
71
|
+
console.log(`[xyz loader] would load "${ctx.name}" (${fileId})`, ctx.components);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Equivalent declarative form — hand the whole registry over at once:
|
|
75
|
+
// modelsList.loaders = { xyz: async (fileId, ctx) => { ... } };
|
|
76
|
+
//
|
|
77
|
+
// Registering a loader for an extension gives its files a load action here —
|
|
78
|
+
// but doesn't control visibility (see the note above). For a CONVERT-style
|
|
79
|
+
// action — e.g. a PLY that runs a PLY→3D-tiles cloud component then views the
|
|
80
|
+
// result — register a RICH entry so the button reads correctly:
|
|
81
|
+
// modelsList.registerLoader("ply", {
|
|
82
|
+
// label: "Generate 3D tiles",
|
|
83
|
+
// icon: "mdi:cube-scan",
|
|
84
|
+
// fn: async (fileId, ctx) => {
|
|
85
|
+
// // const { executionId } = await ctx.client.executeComponent(...);
|
|
86
|
+
// // ...poll until done, then open the produced tileset...
|
|
87
|
+
// },
|
|
88
|
+
// });
|
|
89
|
+
|
|
90
|
+
// ── Extension point 2: override the IFC → fragments converter ────────────────
|
|
91
|
+
// OMIT this to keep the built-in default (drives the project's IfcFragmenter
|
|
92
|
+
// cloud component via the platform client). Provide it to run conversion
|
|
93
|
+
// elsewhere / differently — the panel keeps the progress bar, reload-survival,
|
|
94
|
+
// association and refresh; your converter just `start`s and is `poll`ed.
|
|
95
|
+
modelsList.converter = {
|
|
96
|
+
async start(fileId) {
|
|
97
|
+
// kick off your conversion; return an execution id to poll
|
|
98
|
+
return `exec-${fileId}`;
|
|
99
|
+
},
|
|
100
|
+
async poll(executionId) {
|
|
101
|
+
// report progress; when done, hand back the produced .frag id
|
|
102
|
+
return { done: true, ok: true, fragId: "demo-frag", message: "Converted" };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Mount the panel into a one-area layout. (In a real app it sits beside a
|
|
107
|
+
// top-viewer so loaded models have a world to render into.)
|
|
108
|
+
app.elements = {
|
|
109
|
+
files: () => html`${modelsList}`,
|
|
110
|
+
};
|
|
111
|
+
app.layouts = {
|
|
112
|
+
main: {
|
|
113
|
+
label: "Files",
|
|
114
|
+
icon: "mdi:folder-multiple-outline",
|
|
115
|
+
template: `"files" 1fr / 1fr`,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
app.layout = "main";
|
|
119
|
+
|
|
120
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
121
|
+
container.appendChild(app);
|
|
122
|
+
document.body.style.margin = "0";
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## topics-panel
|
|
3
|
+
---
|
|
4
|
+
Every coordination workflow needs a place to track issues, clashes, and
|
|
5
|
+
action items against a 3D model — who's responsible, what still needs
|
|
6
|
+
review, and where in the model it actually is. Building that from scratch
|
|
7
|
+
means a BCF-compatible data model, a form, comment threads, viewpoint
|
|
8
|
+
capture/navigation, document links, and a persistence layer — all before a
|
|
9
|
+
single issue gets tracked.
|
|
10
|
+
|
|
11
|
+
`topics-panel` is a ready-made web component that delivers exactly that. It
|
|
12
|
+
connects to `@thatopen/components`' `BCFTopics` (and `Viewpoints`)
|
|
13
|
+
automatically and manages the entire workflow: the topic list with search,
|
|
14
|
+
full CRUD on topics/comments/viewpoints/documents, camera navigation,
|
|
15
|
+
BCF export/import, and auto-save — without you writing any UI code.
|
|
16
|
+
|
|
17
|
+
This tutorial covers the prerequisites before mounting the panel; dropping
|
|
18
|
+
it into a `top-app` layout alongside a 3D viewer; and a breakdown of
|
|
19
|
+
everything the panel handles automatically so you know what you do not need
|
|
20
|
+
to implement yourself.
|
|
21
|
+
|
|
22
|
+
By the end, you'll have a fully working topics/issues environment running
|
|
23
|
+
in your application with a single line of markup.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { html } from "lit";
|
|
27
|
+
import * as THREE from "three";
|
|
28
|
+
import * as OBC from "@thatopen/components";
|
|
29
|
+
import * as OBF from "@thatopen/components-front";
|
|
30
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
31
|
+
import * as BUI from "@thatopen/ui";
|
|
32
|
+
import { PlatformClient, UIManager, TopicsManager } from "@thatopen/services";
|
|
33
|
+
import type { App } from "../app/index";
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/* MD
|
|
37
|
+
### ✅ Prerequisites
|
|
38
|
+
|
|
39
|
+
Two conditions must be met before the panel mounts:
|
|
40
|
+
|
|
41
|
+
1. **`UIManager` must be in the setup call.** It registers all platform web
|
|
42
|
+
components, including `<top-topics-panel>` itself. Without it the
|
|
43
|
+
element is unknown to the browser and renders as an empty box.
|
|
44
|
+
|
|
45
|
+
2. **`TopicsManager.init(client)` must have resolved.** It configures
|
|
46
|
+
`BCFTopics` (version, author, assignable users — all from real project
|
|
47
|
+
data) and loads whatever BCF was previously saved for this project. The
|
|
48
|
+
panel mounts into an empty state and never catches up on its own if you
|
|
49
|
+
load a BCF after the fact; it only subscribes to change events, it does
|
|
50
|
+
not poll.
|
|
51
|
+
|
|
52
|
+
Both conditions are naturally satisfied when you call `init` before
|
|
53
|
+
appending `top-app` to the document, which is the pattern shown below.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const client = PlatformClient.fromPlatformContext();
|
|
57
|
+
|
|
58
|
+
// The `{ uuid }` entries tell the platform which built-ins to register for
|
|
59
|
+
// this app — one per built-in beyond the "stable" `@thatopen/components`
|
|
60
|
+
// globals passed as the first argument.
|
|
61
|
+
const { components } = (await client.setup(
|
|
62
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
63
|
+
{ uuid: UIManager.uuid },
|
|
64
|
+
{ uuid: TopicsManager.uuid },
|
|
65
|
+
)) as { components: OBC.Components };
|
|
66
|
+
|
|
67
|
+
components.get(UIManager).init();
|
|
68
|
+
|
|
69
|
+
// init must resolve before top-app (and therefore topics-panel) mounts.
|
|
70
|
+
await components.get(TopicsManager).init(client);
|
|
71
|
+
|
|
72
|
+
/* MD
|
|
73
|
+
### 🖥️ Wiring the panel
|
|
74
|
+
|
|
75
|
+
Add `topics-panel` as a named area in `app.elements` and reference it in
|
|
76
|
+
your layout template, next to the 3D viewer — the panel controls the
|
|
77
|
+
camera and element selection directly in the scene when you click a
|
|
78
|
+
viewpoint.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
82
|
+
|
|
83
|
+
app.setup = (waitUntil) => {
|
|
84
|
+
waitUntil(
|
|
85
|
+
(async () => {
|
|
86
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
87
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
88
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
89
|
+
})(),
|
|
90
|
+
"Fragments Core",
|
|
91
|
+
);
|
|
92
|
+
return { components, client };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
app.elements = {
|
|
96
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
97
|
+
topics: () => html`<top-topics-panel></top-topics-panel>`,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
app.layouts = {
|
|
101
|
+
main: {
|
|
102
|
+
label: "Main",
|
|
103
|
+
icon: "solar:3d-square-bold",
|
|
104
|
+
template: `"viewer topics" 1fr / 1fr 30rem`,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
app.layout = "main";
|
|
109
|
+
|
|
110
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
111
|
+
container.appendChild(app);
|
|
112
|
+
document.body.style.margin = "0";
|
|
113
|
+
|
|
114
|
+
/* MD
|
|
115
|
+
### 🤖 What's handled automatically
|
|
116
|
+
|
|
117
|
+
**`TopicsManager`** resolves `Viewpoints.world` in the background once
|
|
118
|
+
`top-viewer` creates it (which only happens after `top-app` finishes
|
|
119
|
+
mounting), including backfilling it onto viewpoints that were restored
|
|
120
|
+
from the saved BCF before the world existed — `Viewpoint.go()` would
|
|
121
|
+
otherwise silently no-op for every one of them. Nothing to wire yourself.
|
|
122
|
+
|
|
123
|
+
**`topics-panel`**, once mounted, takes ownership of the following without
|
|
124
|
+
any further code from you:
|
|
125
|
+
|
|
126
|
+
**Full topic CRUD** — create, edit, and delete (behind a confirmation
|
|
127
|
+
modal) topics with title, type, status, priority, assigned-to, due date,
|
|
128
|
+
and stage. The list shows a search bar, a type badge next to each title,
|
|
129
|
+
and switches to an empty state with a "New topic" call-to-action when
|
|
130
|
+
there's nothing yet.
|
|
131
|
+
|
|
132
|
+
**Comments** — add, edit, and delete (with confirmation) threaded comments
|
|
133
|
+
on a topic, each optionally linked to a specific viewpoint via a clickable
|
|
134
|
+
tag that jumps the camera there.
|
|
135
|
+
|
|
136
|
+
**Viewpoints** — capture the current camera + selection as a new
|
|
137
|
+
viewpoint, browse them in a snapshot carousel, and click one to fly the
|
|
138
|
+
camera and re-apply its selection highlight. Navigation is a no-op (not an
|
|
139
|
+
error) if no models are loaded yet, so a stray click can't fly the camera
|
|
140
|
+
toward nothing.
|
|
141
|
+
|
|
142
|
+
**Documents** — link files from the project's CDE storage (searchable tree
|
|
143
|
+
picker), unlink from just this topic or hard-delete everywhere it's
|
|
144
|
+
referenced, and download. Linking is genuinely shared: the same file
|
|
145
|
+
referenced by two topics reuses one document entry instead of duplicating
|
|
146
|
+
it.
|
|
147
|
+
|
|
148
|
+
**BCF export/import** — export the selected topics (or, with nothing
|
|
149
|
+
selected, the whole project) to a single `.bcf` file named after the
|
|
150
|
+
project or the lone topic's title. Import merges a `.bcf` into the current
|
|
151
|
+
project instead of replacing it; a topic sharing a GUID with an existing
|
|
152
|
+
one is overwritten (treated as an update from elsewhere, not a conflict).
|
|
153
|
+
|
|
154
|
+
**Persistence** — every mutation is auto-saved through `top-app`'s
|
|
155
|
+
`projectStorageContext`, debounced per file so rapid edits collapse into
|
|
156
|
+
one upload. A small "Saving..." indicator (shared with any other built-in
|
|
157
|
+
using the same context, e.g. `clashes-panel`) reflects the real
|
|
158
|
+
debounce-then-upload cycle: just the icon while pending, the full pill
|
|
159
|
+
while actually uploading.
|
|
160
|
+
|
|
161
|
+
**Author/assignee names** — emails resolve to real names (with an
|
|
162
|
+
initials avatar) via `top-app`'s `projectDataContext`, which already
|
|
163
|
+
covers the project owner as well as regular collaborators — no separate
|
|
164
|
+
lookup needed.
|
|
165
|
+
|
|
166
|
+
**Cross-panel awareness** — the list updates live even when a topic is
|
|
167
|
+
created somewhere else entirely (e.g. `clashes-panel`'s "Create topic from
|
|
168
|
+
clash"), including the empty-state → list transition if this was the
|
|
169
|
+
project's first topic.
|
|
170
|
+
*/
|