@thatopen/services 0.17.0 → 0.18.0
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 +1043 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +7 -6
- package/docs/builtin/paths.json +16 -0
- package/package.json +2 -1
- package/src/built-in/GISManager/example.ts +326 -0
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +5 -2
- package/src/built-in/UIManager/src/gis-panel/example.ts +163 -0
- package/src/built-in/UIManager/src/gis-properties-panel/example.ts +168 -0
- package/src/built-in/UIManager/src/map-panel/example.ts +189 -0
- package/src/built-in/UIManager/src/models-list/example.ts +61 -2
- package/src/built-in/UIManager/src/settings-panel/example.ts +5 -3
- package/src/built-in/UIManager/src/viewer-toolbar/example.ts +25 -1
- package/src/built-in/index.ts +1051 -0
|
@@ -73,8 +73,11 @@ app.elements = {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
app.layouts = {
|
|
76
|
-
// Each layout defines its own CSS grid template.
|
|
77
|
-
//
|
|
76
|
+
// Each layout defines its own CSS grid template. The app mounts the UNION
|
|
77
|
+
// of every layout's areas ONCE and switching layouts toggles which are
|
|
78
|
+
// visible (the rest stay mounted with display:none), so panel state
|
|
79
|
+
// survives a switch - element factories run on mount, not per layout.
|
|
80
|
+
// To react to a switch, listen for the `top:layout-changed` event.
|
|
78
81
|
viewer: {
|
|
79
82
|
label: "Viewer",
|
|
80
83
|
icon: "solar:3d-square-bold",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## gis-panel
|
|
3
|
+
---
|
|
4
|
+
A georeferenced project is much easier to review with its real surroundings
|
|
5
|
+
around the model, and with the GIS layers of the site (networks, parcels,
|
|
6
|
+
boundaries) switched on and off from a list. Streaming the terrain and
|
|
7
|
+
buildings needs a Cesium Ion token and a way to load and unload them, and
|
|
8
|
+
fine-tuning their position needs a few controls.
|
|
9
|
+
|
|
10
|
+
`gis-panel` is a ready-made web component that delivers all of that. It
|
|
11
|
+
connects to `GISManager` automatically and gives the user the token input, the
|
|
12
|
+
button that loads the 3D map, the position fine-tune and the list of the
|
|
13
|
+
project's vector layers.
|
|
14
|
+
|
|
15
|
+
This tutorial covers the prerequisites before mounting the panel; dropping it
|
|
16
|
+
into a `top-app` layout next to a 3D viewer; and a breakdown of everything the
|
|
17
|
+
panel handles automatically so you know what you do not need to implement
|
|
18
|
+
yourself.
|
|
19
|
+
|
|
20
|
+
By the end, you'll have the GIS controls of your project running in your
|
|
21
|
+
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, GISManager, ProjectManager } 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-gis-panel>` itself. Without it the element is
|
|
41
|
+
unknown to the browser and renders as an empty box.
|
|
42
|
+
|
|
43
|
+
2. **`ProjectManager.init(client)` must have resolved.** `GISManager` takes
|
|
44
|
+
the project origin from it: without it the 3D tiles and the vector layers
|
|
45
|
+
are placed around a default origin instead of the project's.
|
|
46
|
+
|
|
47
|
+
3. **`GISManager.init(client)` must have resolved**, after `ProjectManager`.
|
|
48
|
+
The panel reads the manager's state when it is
|
|
49
|
+
connected to the document (the token, the tiles state, the tiles offset and
|
|
50
|
+
the layer list), then follows its events. Without `init`, the manifest is
|
|
51
|
+
never read, so the layer list stays empty, and the tiles offset cannot be
|
|
52
|
+
saved.
|
|
53
|
+
|
|
54
|
+
Both are satisfied by awaiting the two `init`s, in that order, before
|
|
55
|
+
appending `top-app` to the document, which is the pattern shown below.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const client = PlatformClient.fromPlatformContext();
|
|
59
|
+
|
|
60
|
+
const { components } = (await client.setup(
|
|
61
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
62
|
+
{ uuid: UIManager.uuid },
|
|
63
|
+
{ uuid: ProjectManager.uuid },
|
|
64
|
+
{ uuid: GISManager.uuid },
|
|
65
|
+
)) as { components: OBC.Components };
|
|
66
|
+
|
|
67
|
+
components.get(UIManager).init();
|
|
68
|
+
|
|
69
|
+
// Order matters: GISManager reads the origin that ProjectManager has loaded.
|
|
70
|
+
await components.get(ProjectManager).init(client);
|
|
71
|
+
await components.get(GISManager).init(client);
|
|
72
|
+
|
|
73
|
+
/* MD
|
|
74
|
+
### 🖥️ Wiring the panel
|
|
75
|
+
|
|
76
|
+
Add `gis-panel` as a named area in `app.elements` and reference it in your
|
|
77
|
+
layout template. A narrow column on the left of the 3D viewer is the natural
|
|
78
|
+
arrangement: the tiles and the layers are drawn in the viewer's scene.
|
|
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
|
+
|
|
93
|
+
return { components, client };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
app.elements = {
|
|
97
|
+
gis: () => html`<top-gis-panel></top-gis-panel>`,
|
|
98
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
app.layouts = {
|
|
102
|
+
main: {
|
|
103
|
+
label: "Main",
|
|
104
|
+
icon: "mdi:earth",
|
|
105
|
+
template: `"gis viewer" 1fr / 22rem 1fr`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
app.layout = "main";
|
|
110
|
+
|
|
111
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
112
|
+
container.appendChild(app);
|
|
113
|
+
document.body.style.margin = "0";
|
|
114
|
+
|
|
115
|
+
/* MD
|
|
116
|
+
### 🤖 What the panel manages automatically
|
|
117
|
+
|
|
118
|
+
Once mounted, `gis-panel` takes ownership of the following without any
|
|
119
|
+
further code from you:
|
|
120
|
+
|
|
121
|
+
**Cesium Ion token** — a password field holds the token. It is written to
|
|
122
|
+
`GISManager.apiToken` as the user types. The panel does not store it: if
|
|
123
|
+
you want it to survive a reload, set `gis.apiToken` yourself before the
|
|
124
|
+
panel mounts.
|
|
125
|
+
|
|
126
|
+
**Loading the 3D map** — the "Load GIS Map" button calls `enableTiles()`
|
|
127
|
+
(disabled until there is a token) and turns into "Unload GIS Map", which calls
|
|
128
|
+
`disableTiles()`. While the tiles load the button shows its loading state. A
|
|
129
|
+
success toast confirms the load, and a failure shows the readable message
|
|
130
|
+
coming from `onLoadError` (invalid token, no access to the tileset, rate
|
|
131
|
+
limit, no connection).
|
|
132
|
+
|
|
133
|
+
**Tiles fine-tune offset** — three sliders (East, North, Up, in metres) move
|
|
134
|
+
the tiles through `setTilesOffset`. Once the offset differs from the saved
|
|
135
|
+
one, a "Save offset" button appears. It calls `saveTilesOffset()`, which
|
|
136
|
+
writes the offset into the project's layer manifest, and a toast reports
|
|
137
|
+
whether the save worked. The offset is restored by `GISManager.init` the next
|
|
138
|
+
time the project is opened.
|
|
139
|
+
|
|
140
|
+
**Vector layers** — a table lists the layers declared in the manifest
|
|
141
|
+
(`vectorLayersConfig`), each with a checkbox. Checking it loads the layer
|
|
142
|
+
(`loadVectorLayer`), unchecking it removes it (`unloadVectorLayer`), and the
|
|
143
|
+
row shows "(loading…)" in between. If a layer cannot be read, for example an
|
|
144
|
+
unsupported coordinate system, an error toast carries the reason reported by
|
|
145
|
+
`onVectorLayerError`. The list updates by itself if the manifest is read
|
|
146
|
+
after the panel mounts.
|
|
147
|
+
|
|
148
|
+
**Registering layers** — the panel only lists and toggles the layers the
|
|
149
|
+
manifest already declares. There is no user interface to upload a shapefile or
|
|
150
|
+
a GeoJSON file, name it and style it yet: your application writes the
|
|
151
|
+
manifest (see the `GISManager` tutorial for its format).
|
|
152
|
+
|
|
153
|
+
**Origin** — the project origin is not edited here. It belongs to
|
|
154
|
+
`ProjectManager`, and it is set from the "Coordinates" section of the settings
|
|
155
|
+
panel. The tiles and the layers follow it live.
|
|
156
|
+
|
|
157
|
+
**Notifications** — every toast is a `top:notification` event that bubbles up
|
|
158
|
+
to `top-app`, which displays it.
|
|
159
|
+
|
|
160
|
+
**Disconnection cleanup** — when the panel is removed from the DOM it
|
|
161
|
+
unsubscribes from every `GISManager` event. The 3D map and the layers stay
|
|
162
|
+
loaded.
|
|
163
|
+
*/
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## gis-properties-panel
|
|
3
|
+
---
|
|
4
|
+
A GIS feature (a pipe, a parcel, a manhole) is useless without its
|
|
5
|
+
attributes: diameter, material, owner, installation date. Users want to
|
|
6
|
+
click a feature, in the 3D view or on the map, and read them.
|
|
7
|
+
|
|
8
|
+
`gis-properties-panel` is a ready-made web component that delivers exactly
|
|
9
|
+
that. It connects to `GISManager` automatically and shows the attribute table of
|
|
10
|
+
the selected vector feature, with a filter and readable values.
|
|
11
|
+
|
|
12
|
+
This tutorial covers the prerequisites before mounting the panel; dropping it
|
|
13
|
+
into a `top-app` layout next to the map; a breakdown of everything the panel
|
|
14
|
+
handles automatically; and how an application can use one properties column
|
|
15
|
+
for both BIM and GIS selections.
|
|
16
|
+
|
|
17
|
+
By the end, you'll have GIS attributes on screen with a single line of markup.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { html } from "lit";
|
|
21
|
+
import * as THREE from "three";
|
|
22
|
+
import * as OBC from "@thatopen/components";
|
|
23
|
+
import * as OBF from "@thatopen/components-front";
|
|
24
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
25
|
+
import * as BUI from "@thatopen/ui";
|
|
26
|
+
import { PlatformClient, UIManager, GISManager, ProjectManager } from "@thatopen/services";
|
|
27
|
+
import type { App } from "../app/index";
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/* MD
|
|
31
|
+
### ✅ Prerequisites
|
|
32
|
+
|
|
33
|
+
Three conditions must be met before the panel mounts:
|
|
34
|
+
|
|
35
|
+
1. **`UIManager` must be in the setup call.** It registers all platform web
|
|
36
|
+
components, including `<top-gis-properties-panel>` itself. Without it the element is
|
|
37
|
+
unknown to the browser and renders as an empty box.
|
|
38
|
+
|
|
39
|
+
2. **`ProjectManager.init(client)` must have resolved.** `GISManager` takes
|
|
40
|
+
the project origin from it: without it the 3D tiles and the vector layers
|
|
41
|
+
are placed around a default origin instead of the project's.
|
|
42
|
+
|
|
43
|
+
3. **`GISManager.init(client)` must have resolved**, after `ProjectManager`.
|
|
44
|
+
The panel itself only listens to selections, but
|
|
45
|
+
without `init` the manifest is never read, so there are no layers to
|
|
46
|
+
select. A panel created after a selection already exists shows that
|
|
47
|
+
selection straight away.
|
|
48
|
+
|
|
49
|
+
Both are satisfied by awaiting the two `init`s, in that order, before
|
|
50
|
+
appending `top-app` to the document, which is the pattern shown below.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const client = PlatformClient.fromPlatformContext();
|
|
54
|
+
|
|
55
|
+
const { components } = (await client.setup(
|
|
56
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
57
|
+
{ uuid: UIManager.uuid },
|
|
58
|
+
{ uuid: ProjectManager.uuid },
|
|
59
|
+
{ uuid: GISManager.uuid },
|
|
60
|
+
)) as { components: OBC.Components };
|
|
61
|
+
|
|
62
|
+
components.get(UIManager).init();
|
|
63
|
+
|
|
64
|
+
// Order matters: GISManager reads the origin that ProjectManager has loaded.
|
|
65
|
+
await components.get(ProjectManager).init(client);
|
|
66
|
+
await components.get(GISManager).init(client);
|
|
67
|
+
|
|
68
|
+
/* MD
|
|
69
|
+
### 🖥️ Wiring the panel
|
|
70
|
+
|
|
71
|
+
Add `gis-properties-panel` as a named area. Next to the map is the usual
|
|
72
|
+
place: the user clicks a feature on the map or in the 3D view, and reads
|
|
73
|
+
its attributes in the column beside it. Selecting features on the map requires
|
|
74
|
+
`map-panel` (and, as it is documented there, see-through terrain).
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
78
|
+
|
|
79
|
+
app.setup = (waitUntil) => {
|
|
80
|
+
waitUntil(
|
|
81
|
+
(async () => {
|
|
82
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
83
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
84
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
85
|
+
})(),
|
|
86
|
+
"Fragments Core",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return { components, client };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
app.elements = {
|
|
93
|
+
gis: () => html`<top-gis-panel></top-gis-panel>`,
|
|
94
|
+
viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
95
|
+
map: () => html`<top-map-panel></top-map-panel>`,
|
|
96
|
+
properties: () => html`<top-gis-properties-panel></top-gis-properties-panel>`,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
app.layouts = {
|
|
100
|
+
main: {
|
|
101
|
+
label: "Main",
|
|
102
|
+
icon: "mdi:information-outline",
|
|
103
|
+
template: `"gis viewer map" 1fr "gis viewer properties" 1fr / 20rem 1fr 26rem`,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
app.layout = "main";
|
|
108
|
+
|
|
109
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
110
|
+
container.appendChild(app);
|
|
111
|
+
document.body.style.margin = "0";
|
|
112
|
+
|
|
113
|
+
/* MD
|
|
114
|
+
### 🤖 What the panel manages automatically
|
|
115
|
+
|
|
116
|
+
Once mounted, `gis-properties-panel` takes ownership of the following without
|
|
117
|
+
any further code from you:
|
|
118
|
+
|
|
119
|
+
**Selection tracking** — the panel subscribes to `GISManager.onFeatureSelected`,
|
|
120
|
+
so it shows the selected feature whether the click came from the 3D view, the
|
|
121
|
+
map, or your own call to `selectVectorFeature`.
|
|
122
|
+
|
|
123
|
+
**Attribute table** — the section is titled with the layer's name, shows the
|
|
124
|
+
geometry type, and lists every attribute of the feature as a name and a value.
|
|
125
|
+
|
|
126
|
+
**Filter** — a search box filters the table by attribute name or by visible
|
|
127
|
+
value.
|
|
128
|
+
|
|
129
|
+
**Value formatting** — numbers and text are shown as they are, booleans as
|
|
130
|
+
Yes/No, dates as `YYYY-MM-DD` (with the time when they have one) and complex
|
|
131
|
+
values as JSON. Empty values (null, blank text, invalid dates, non-finite
|
|
132
|
+
numbers) are shown as a dimmed dash, and are not matched by the filter.
|
|
133
|
+
|
|
134
|
+
**Empty state** — with no selection the panel explains that an element of a
|
|
135
|
+
vector layer has to be selected, and it goes back to that state when the
|
|
136
|
+
selection is cleared.
|
|
137
|
+
|
|
138
|
+
**Disconnection cleanup** — when the panel is removed from the DOM it
|
|
139
|
+
unsubscribes from the manager.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/* MD
|
|
143
|
+
### 🔀 One properties column for BIM and GIS
|
|
144
|
+
|
|
145
|
+
The panel only knows about GIS features. If your application has a single
|
|
146
|
+
properties column, switch what it shows from the selection events: the GIS
|
|
147
|
+
panel when a vector feature is selected, the BIM `top-properties-panel` when
|
|
148
|
+
a BIM element is. Keep both elements alive and swap them inside one stable
|
|
149
|
+
container.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
const gis = components.get(GISManager);
|
|
153
|
+
|
|
154
|
+
const bimProperties = document.createElement("top-properties-panel");
|
|
155
|
+
const gisProperties = document.createElement("top-gis-properties-panel");
|
|
156
|
+
const column = document.createElement("div");
|
|
157
|
+
column.style.height = "100%";
|
|
158
|
+
column.append(bimProperties);
|
|
159
|
+
|
|
160
|
+
gis.onFeatureSelected.add((selection) => {
|
|
161
|
+
if (selection) column.replaceChildren(gisProperties);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
components.get(OBF.Highlighter).events.select.onHighlight.add(() => {
|
|
165
|
+
column.replaceChildren(bimProperties);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Render `column` from the `properties` element of the layout above.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## map-panel
|
|
3
|
+
---
|
|
4
|
+
In a large site, the 3D camera alone is a poor way to get oriented: users
|
|
5
|
+
want a plan view to see where they are, where the GIS features are, and to
|
|
6
|
+
jump somewhere else with a click.
|
|
7
|
+
|
|
8
|
+
`map-panel` is a ready-made web component that delivers exactly that: a
|
|
9
|
+
top-down, north-up map of the GIS tiles with the project's vector layers drawn
|
|
10
|
+
over it, a marker for the 3D camera, and your own markers. It connects to
|
|
11
|
+
`GISManager` automatically, and everything on it stays synchronized with
|
|
12
|
+
the 3D view.
|
|
13
|
+
|
|
14
|
+
This tutorial covers the prerequisites before mounting the panel; dropping it
|
|
15
|
+
into a `top-app` layout beside the 3D viewer; a breakdown of everything the
|
|
16
|
+
panel handles automatically; and how an application adds its own markers.
|
|
17
|
+
|
|
18
|
+
By the end, you'll have an interactive minimap in your application with a
|
|
19
|
+
single line of markup.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { html } from "lit";
|
|
23
|
+
import * as THREE from "three";
|
|
24
|
+
import * as OBC from "@thatopen/components";
|
|
25
|
+
import * as OBF from "@thatopen/components-front";
|
|
26
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
27
|
+
import * as BUI from "@thatopen/ui";
|
|
28
|
+
import { PlatformClient, UIManager, GISManager, ProjectManager } from "@thatopen/services";
|
|
29
|
+
import type { App } from "../app/index";
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/* MD
|
|
33
|
+
### ✅ Prerequisites
|
|
34
|
+
|
|
35
|
+
Three conditions must be met before the panel mounts:
|
|
36
|
+
|
|
37
|
+
1. **`UIManager` must be in the setup call.** It registers all platform web
|
|
38
|
+
components, including `<top-map-panel>` itself. Without it the element is
|
|
39
|
+
unknown to the browser and renders as an empty box.
|
|
40
|
+
|
|
41
|
+
2. **`ProjectManager.init(client)` must have resolved.** `GISManager` takes
|
|
42
|
+
the project origin from it: without it the 3D tiles and the vector layers
|
|
43
|
+
are placed around a default origin instead of the project's.
|
|
44
|
+
|
|
45
|
+
3. **`GISManager.init(client)` must have resolved**, after `ProjectManager`.
|
|
46
|
+
The panel needs the 3D tiles to capture the map, and
|
|
47
|
+
`init` is what reads the layer manifest: the captured area is the box
|
|
48
|
+
of all the manifest's layers, so without `init` the area falls back to a
|
|
49
|
+
square around the origin and no layers are drawn over the map.
|
|
50
|
+
|
|
51
|
+
Both are satisfied by awaiting the two `init`s, in that order, before
|
|
52
|
+
appending `top-app` to the document, which is the pattern shown below.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const client = PlatformClient.fromPlatformContext();
|
|
56
|
+
|
|
57
|
+
const { components } = (await client.setup(
|
|
58
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
59
|
+
{ uuid: UIManager.uuid },
|
|
60
|
+
{ uuid: ProjectManager.uuid },
|
|
61
|
+
{ uuid: GISManager.uuid },
|
|
62
|
+
)) as { components: OBC.Components };
|
|
63
|
+
|
|
64
|
+
components.get(UIManager).init();
|
|
65
|
+
|
|
66
|
+
// Order matters: GISManager reads the origin that ProjectManager has loaded.
|
|
67
|
+
await components.get(ProjectManager).init(client);
|
|
68
|
+
await components.get(GISManager).init(client);
|
|
69
|
+
|
|
70
|
+
/* MD
|
|
71
|
+
### 🖥️ Wiring the panel
|
|
72
|
+
|
|
73
|
+
Add `map-panel` as a named area next to the viewer. The map can only be
|
|
74
|
+
captured once the 3D tiles are loaded, so this layout also mounts
|
|
75
|
+
`gis-panel`, which lets the user enter the token and load the map. Your
|
|
76
|
+
application can do the same by code with `GISManager.enableTiles()`. The
|
|
77
|
+
viewer also carries `top-viewer-gis-ghost-button` in a toolbar, which makes the
|
|
78
|
+
map see-through so its vector features can be picked.
|
|
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
|
+
|
|
93
|
+
return { components, client };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
app.elements = {
|
|
97
|
+
gis: () => html`<top-gis-panel></top-gis-panel>`,
|
|
98
|
+
viewer: () => html`
|
|
99
|
+
<top-viewer>
|
|
100
|
+
<top-viewer-tools></top-viewer-tools>
|
|
101
|
+
<top-viewer-toolbar headless>
|
|
102
|
+
<top-viewer-gis-ghost-button></top-viewer-gis-ghost-button>
|
|
103
|
+
</top-viewer-toolbar>
|
|
104
|
+
</top-viewer>
|
|
105
|
+
`,
|
|
106
|
+
map: () => html`<top-map-panel></top-map-panel>`,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
app.layouts = {
|
|
110
|
+
main: {
|
|
111
|
+
label: "Main",
|
|
112
|
+
icon: "mdi:map-search-outline",
|
|
113
|
+
template: `"gis viewer map" 1fr / 20rem 1fr 26rem`,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
app.layout = "main";
|
|
118
|
+
|
|
119
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
120
|
+
container.appendChild(app);
|
|
121
|
+
document.body.style.margin = "0";
|
|
122
|
+
|
|
123
|
+
/* MD
|
|
124
|
+
### 🤖 What the panel manages automatically
|
|
125
|
+
|
|
126
|
+
Once mounted, `map-panel` takes ownership of the following without any
|
|
127
|
+
further code from you:
|
|
128
|
+
|
|
129
|
+
**Capturing the map** — a "Capture map" button calls `minimap.captureMap()`.
|
|
130
|
+
It is disabled until the 3D tiles are loaded, with a hint saying so, and a
|
|
131
|
+
failed capture is reported as a toast. A map that has already been captured
|
|
132
|
+
stays on screen if the tiles are unloaded afterwards. The area is the box of
|
|
133
|
+
all the manifest's layers (a square around the origin when there are none),
|
|
134
|
+
and the image has a fixed resolution.
|
|
135
|
+
|
|
136
|
+
**Pan and zoom** — the wheel zooms around the cursor and dragging pans. The
|
|
137
|
+
zoom is limited by the image's resolution: the map never magnifies the image
|
|
138
|
+
beyond the point where it is just blur.
|
|
139
|
+
|
|
140
|
+
**Live vector layers** — the loaded layers are drawn as 2D vectors under the
|
|
141
|
+
captured image (so they stay crisp at any zoom), with the style of the
|
|
142
|
+
manifest. Where the terrain is opaque it hides them, just like in 3D.
|
|
143
|
+
|
|
144
|
+
**Hover and selection, in sync with 3D** — moving over a feature highlights it
|
|
145
|
+
on the map and in the 3D view, and the selected feature is highlighted in both.
|
|
146
|
+
Hovering and clicking features on the map only work while the terrain is
|
|
147
|
+
see-through, since you cannot pick what you cannot see. The
|
|
148
|
+
`<top-viewer-gis-ghost-button>` placed in the viewer toolbar above toggles it
|
|
149
|
+
(it is disabled until the tiles are loaded). An application without a toolbar
|
|
150
|
+
can call `GISManager.setTilesOpacity` with a value below 1 instead.
|
|
151
|
+
|
|
152
|
+
**Click to select and focus** — a click on a feature selects it, exactly like a
|
|
153
|
+
3D click (`onFeatureSelected` fires), and the 3D camera flies to it while the
|
|
154
|
+
map zooms to the same neighbourhood. A click on empty ground moves the 3D
|
|
155
|
+
camera there, keeping its height and viewing direction, and leaves the
|
|
156
|
+
selection alone.
|
|
157
|
+
|
|
158
|
+
**Camera indicator** — an arrow and a view cone show where the 3D camera is and
|
|
159
|
+
where it looks, live.
|
|
160
|
+
|
|
161
|
+
**Markers** — the markers added to `GISManager.minimap` are drawn over the map
|
|
162
|
+
and follow its zoom and pan (see the next section). Markers without their own
|
|
163
|
+
element are drawn as a default badge showing the label.
|
|
164
|
+
|
|
165
|
+
**Disconnection cleanup** — when the panel is removed from the DOM it
|
|
166
|
+
unsubscribes from the manager and clears the hover it had set. The capture and
|
|
167
|
+
the markers live in `GISManager`, so a recreated panel (for example after the
|
|
168
|
+
user switches layout) shows them again straight away.
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/* MD
|
|
172
|
+
### 📍 Adding your own markers
|
|
173
|
+
|
|
174
|
+
The markers belong to `GISManager.minimap`, not to the panel. Add them with a
|
|
175
|
+
scene position (`vectorLayers.geoToWorld` converts a latitude and a longitude)
|
|
176
|
+
and, optionally, a `createElement` factory to draw them your way. Without the
|
|
177
|
+
factory the panel draws a badge with the label and reports the clicks through
|
|
178
|
+
`onMarkerClicked`.
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
const gis = components.get(GISManager);
|
|
182
|
+
|
|
183
|
+
gis.minimap.addMarker({
|
|
184
|
+
id: "site-office",
|
|
185
|
+
label: "Site office",
|
|
186
|
+
position: gis.vectorLayers.geoToWorld(gis.latitude, gis.longitude, gis.height),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
gis.minimap.onMarkerClicked.add((id) => console.log("marker clicked:", id));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
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
9
|
|
|
10
|
-
Known gaps (intentionally deferred, not forgotten):
|
|
10
|
+
Known gaps (intentionally deferred, not forgotten): the base-model/auto-coordinate settings aren't ported, and the panel has no default upload of its own: `allow-upload` is opt-in (off by default) and adds an "Upload file" item to the row menu of the root and folder rows, which hands each picked file to the uploader you registered for its extension (see the uploader section below) — files with no uploader aren't uploaded. 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
11
|
|
|
12
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
13
|
*/
|
|
@@ -57,7 +57,9 @@ const modelsList = document.createElement("top-models-list") as ModelsListV2;
|
|
|
57
57
|
// ── Extension point 1: a custom loader, keyed by file extension ──────────────
|
|
58
58
|
// A ModelLoader is `(fileId, ctx) => void | Promise<void>`. The panel calls it
|
|
59
59
|
// when the user loads a file whose extension matches. `ctx` gives you everything
|
|
60
|
-
// you need: { name, ext, components, client, getAlignment, setAlignment }.
|
|
60
|
+
// you need: { name, ext, components, client, getAlignment, setAlignment, progress }.
|
|
61
|
+
// For a long-running load call `ctx.progress(percent, label?)` to show it on the
|
|
62
|
+
// row (omit `percent` while unknown). Do
|
|
61
63
|
// whatever it takes to put the file in the scene — fetch it via the client,
|
|
62
64
|
// parse it, and add geometry to a world from `components`.
|
|
63
65
|
modelsList.registerLoader("xyz", async (fileId, ctx) => {
|
|
@@ -87,6 +89,63 @@ modelsList.registerLoader("xyz", async (fileId, ctx) => {
|
|
|
87
89
|
// },
|
|
88
90
|
// });
|
|
89
91
|
|
|
92
|
+
// ── Upload: opt in, then register an uploader per file extension ─────────────
|
|
93
|
+
// `allow-upload` adds an "Upload file" item to the row menu of the root and
|
|
94
|
+
// folder rows. It opens a file picker and hands each picked file to the
|
|
95
|
+
// uploader registered for its extension, which fully owns that upload. Files
|
|
96
|
+
// whose extension has no uploader are not uploaded (the user gets a warning).
|
|
97
|
+
// `ctx` carries { name, ext, folderId, projectId, components, client };
|
|
98
|
+
// `folderId` is undefined when the upload was started from the root row. While
|
|
99
|
+
// the uploader runs, the panel shows a temporary row for the file; call
|
|
100
|
+
// `ctx.progress(percent, label?)` to drive its progress bar (omit `percent`
|
|
101
|
+
// while unknown for a spinner). The list refreshes once an uploader has run.
|
|
102
|
+
modelsList.setAttribute("allow-upload", "");
|
|
103
|
+
modelsList.registerUploader("ifc", async (file, ctx) => {
|
|
104
|
+
// Create the file in `ctx.folderId` (or the project root) with `ctx.client`,
|
|
105
|
+
// upload `file`'s bytes, and report success/failure however your app does.
|
|
106
|
+
ctx.progress(undefined, "Preparing…");
|
|
107
|
+
ctx.progress(50);
|
|
108
|
+
console.log(`[ifc uploader] would upload "${file.name}" to`, ctx.folderId ?? "the project root");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Declarative form: modelsList.uploaders = { ifc: async (file, ctx) => { ... } };
|
|
112
|
+
|
|
113
|
+
// ── File actions: extra entries in a file row's three-dots menu ──────────────
|
|
114
|
+
// The menu already holds the panel's own items (fit camera, attach/detach); the
|
|
115
|
+
// actions you register for an extension are added after them. Pass a fixed list,
|
|
116
|
+
// or a resolver that runs every time the menu opens (it may be async — the menu
|
|
117
|
+
// shows a loading state meanwhile). While an action runs the file's row shows
|
|
118
|
+
// its status instead of its buttons: call `ctx.progress(percent, label?)` to
|
|
119
|
+
// drive it, and `ctx.reload()` if the action added/removed/renamed files.
|
|
120
|
+
modelsList.registerActions("ifc", [
|
|
121
|
+
{
|
|
122
|
+
label: "Re-index",
|
|
123
|
+
icon: "mdi:database-refresh-outline",
|
|
124
|
+
run: async (fileId, ctx) => {
|
|
125
|
+
ctx.progress(undefined, "Indexing…");
|
|
126
|
+
// await ctx.client.…
|
|
127
|
+
ctx.progress(100);
|
|
128
|
+
console.log(`[ifc action] re-indexed "${ctx.name}" (${fileId})`);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
modelsList.registerActions("ply", async (fileId, ctx) => {
|
|
133
|
+
// Build the menu from state fetched when it opens, e.g. whether a derived
|
|
134
|
+
// file already exists for `fileId` (using `ctx.client`).
|
|
135
|
+
const alreadyConverted = false;
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
label: "Regenerate 3D tiles",
|
|
139
|
+
icon: "mdi:cube-scan",
|
|
140
|
+
disabled: !alreadyConverted,
|
|
141
|
+
run: async (_id, actionCtx) => {
|
|
142
|
+
actionCtx.progress(25, "Converting…");
|
|
143
|
+
await actionCtx.reload();
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
});
|
|
148
|
+
|
|
90
149
|
// ── Extension point 2: override the IFC → fragments converter ────────────────
|
|
91
150
|
// OMIT this to keep the built-in default (drives the project's IfcFragmenter
|
|
92
151
|
// cloud component via the platform client). Provide it to run conversion
|
|
@@ -94,10 +94,12 @@ components.get(UIManager).init();
|
|
|
94
94
|
ghost, show all, clear selection) that also registers the actual keyboard
|
|
95
95
|
shortcuts while mounted.
|
|
96
96
|
|
|
97
|
-
**Search box** —
|
|
98
|
-
custom, as long as a
|
|
97
|
+
**Search box** — sits at the top of the panel, above the sections, and
|
|
98
|
+
filters every section's own rows by label, stock or custom, as long as a
|
|
99
|
+
section opts in (see below).
|
|
99
100
|
|
|
100
|
-
**Restore defaults** — a two-click-confirm button
|
|
101
|
+
**Restore defaults** — a two-click-confirm button in the panel header that
|
|
102
|
+
resets every
|
|
101
103
|
section with persisted state (currently: Graphics) back to its defaults.
|
|
102
104
|
|
|
103
105
|
### 🧩 Building your own section
|
|
@@ -245,8 +245,9 @@ document.body.style.margin = "0";
|
|
|
245
245
|
| `<top-viewer-show-button>` | Un-hides the target set |
|
|
246
246
|
| `<top-viewer-isolate-button>` | Shows only the target set |
|
|
247
247
|
| `<top-viewer-ghost-button>` | Renders the target set semi-transparent |
|
|
248
|
+
| `<top-viewer-gis-ghost-button>` | Makes the GIS map (the 3D tiles of `GISManager`) see-through ⇄ opaque; disabled until the tiles are loaded; the optional `opacity` attribute (0 to 1, default `0.3`) sets the see-through level |
|
|
248
249
|
| `<top-viewer-focus-button>` | Frames the camera on the target set |
|
|
249
|
-
| `<top-viewer-reset-button>` |
|
|
250
|
+
| `<top-viewer-reset-button>` | Shows everything and clears ghosting (global); the optional `reset` attribute can also remove sections and measurements (see below) |
|
|
250
251
|
| `<top-viewer-select-button>` | Activates the default Select tool |
|
|
251
252
|
| `<top-viewer-clip-button>` | Clipping planes: face-placing mode by default, or axis planes / a mode menu via `modes` (see below) |
|
|
252
253
|
| `<top-viewer-measure-length-button>` | Activates length measurement |
|
|
@@ -349,4 +350,27 @@ document.body.style.margin = "0";
|
|
|
349
350
|
same first-person navigation `<top-viewer-walkthrough-button>` toggles.
|
|
350
351
|
- The button's icon always shows the current mode, and the button is active
|
|
351
352
|
while first-person is on. In the menu, the current mode's entry is marked.
|
|
353
|
+
### ♻️ Reset button options
|
|
354
|
+
|
|
355
|
+
By default `<top-viewer-reset-button>` restores full visibility and clears
|
|
356
|
+
ghosting. The optional `reset` attribute lists what it resets — any of
|
|
357
|
+
`visibility`, `sections` (every clipping plane and its section styling)
|
|
358
|
+
and `measurements` (every measurement), whitespace- or comma-separated,
|
|
359
|
+
case-insensitive, unknown tokens ignored:
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<!-- default: visibility only -->
|
|
363
|
+
<top-viewer-reset-button></top-viewer-reset-button>
|
|
364
|
+
|
|
365
|
+
<!-- reset everything -->
|
|
366
|
+
<top-viewer-reset-button reset="visibility sections measurements"></top-viewer-reset-button>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- Parts always run in the order visibility, sections, measurements, and one
|
|
370
|
+
failing part doesn't stop the others.
|
|
371
|
+
- When `sections` or `measurements` is listed, the viewer returns to the
|
|
372
|
+
default Select tool afterwards, so nobody is left inside a half-finished
|
|
373
|
+
measure or clip mode.
|
|
374
|
+
- The tooltip describes what the button resets, and first-person navigation
|
|
375
|
+
is left as it is.
|
|
352
376
|
*/
|