@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.
@@ -73,8 +73,11 @@ app.elements = {
73
73
  };
74
74
 
75
75
  app.layouts = {
76
- // Each layout defines its own CSS grid template. Only areas used in a template
77
- // are rendered for that layout — unused elements are not created.
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): 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.
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 }. Do
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** — filters every section's own rows by label, stock or
98
- custom, as long as a section opts in (see below).
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 that resets every
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>` | Restores full visibility + opacity (global) |
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
  */