@thatopen/services 0.16.1 → 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/cli.js +15 -5
- package/dist/core/client.d.ts +11 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +27 -19
- package/dist/types/files.d.ts +2 -0
- 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 +143 -19
- package/src/built-in/index.ts +1051 -0
- package/src/core/examples/hidden-files.ts +7 -0
|
@@ -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
|
|
@@ -149,28 +149,33 @@ document.body.style.margin = "0";
|
|
|
149
149
|
gotcha below), then appended as the toolbar's light-DOM content:
|
|
150
150
|
|
|
151
151
|
```ts
|
|
152
|
+
import { html, render } from "lit";
|
|
153
|
+
|
|
152
154
|
const headlessBar = document.createElement("top-viewer-toolbar");
|
|
153
155
|
headlessBar.toggleAttribute("headless", true);
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
// A plain container — the `<bim-toolbar>` itself is rendered inside it.
|
|
158
|
+
const toolbarHost = document.createElement("div");
|
|
159
|
+
toolbarHost.style.pointerEvents = "auto";
|
|
157
160
|
render(
|
|
158
161
|
html`
|
|
159
|
-
<bim-toolbar
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<bim-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
<bim-toolbar>
|
|
163
|
+
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
164
|
+
<top-viewer-mode-toggle-button></top-viewer-mode-toggle-button>
|
|
165
|
+
<top-viewer-hide-button></top-viewer-hide-button>
|
|
166
|
+
<top-viewer-select-button></top-viewer-select-button>
|
|
167
|
+
</bim-toolbar-section>
|
|
168
|
+
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
169
|
+
<bim-button
|
|
170
|
+
icon="mdi:fit-to-page-outline"
|
|
171
|
+
@click=${() => myOwnFitToViewLogic()}
|
|
172
|
+
><bim-tooltip placement="top">Zoom to fit</bim-tooltip></bim-button>
|
|
173
|
+
</bim-toolbar-section>
|
|
174
|
+
</bim-toolbar>
|
|
170
175
|
`,
|
|
171
|
-
|
|
176
|
+
toolbarHost,
|
|
172
177
|
);
|
|
173
|
-
headlessBar.appendChild(
|
|
178
|
+
headlessBar.appendChild(toolbarHost);
|
|
174
179
|
// headlessBar can now be appended anywhere inside <top-viewer>.
|
|
175
180
|
```
|
|
176
181
|
|
|
@@ -208,7 +213,7 @@ document.body.style.margin = "0";
|
|
|
208
213
|
Custom Elements v1 forbids for elements created imperatively —
|
|
209
214
|
`document.createElement("bim-button")` throws `NotSupportedError`.
|
|
210
215
|
Render them with lit's `render(html\`...\`, container)` instead, exactly
|
|
211
|
-
like the `
|
|
216
|
+
like the `toolbarHost` container above — never build them by hand.
|
|
212
217
|
- **`bim-button` sizes itself** — square when it's icon-only, a consistent
|
|
213
218
|
height when it has a label. Don't set `width`/`height`/`min-width`
|
|
214
219
|
inline on it; that only fights its own layout and produces buttons that
|
|
@@ -225,7 +230,13 @@ document.body.style.margin = "0";
|
|
|
225
230
|
### 🧩 Standalone buttons reference
|
|
226
231
|
|
|
227
232
|
Every button the default bar uses is independently importable/usable as
|
|
228
|
-
its own custom element
|
|
233
|
+
its own custom element. They render icon-only with a tooltip; set the
|
|
234
|
+
optional `label` attribute on any of them to show text next to the icon
|
|
235
|
+
(the tooltip stays as it is):
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<top-viewer-hide-button label="Hide"></top-viewer-hide-button>
|
|
239
|
+
```
|
|
229
240
|
|
|
230
241
|
| Element | Does |
|
|
231
242
|
|---|---|
|
|
@@ -234,19 +245,132 @@ document.body.style.margin = "0";
|
|
|
234
245
|
| `<top-viewer-show-button>` | Un-hides the target set |
|
|
235
246
|
| `<top-viewer-isolate-button>` | Shows only the target set |
|
|
236
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 |
|
|
237
249
|
| `<top-viewer-focus-button>` | Frames the camera on the target set |
|
|
238
|
-
| `<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) |
|
|
239
251
|
| `<top-viewer-select-button>` | Activates the default Select tool |
|
|
240
|
-
| `<top-viewer-clip-button>` |
|
|
252
|
+
| `<top-viewer-clip-button>` | Clipping planes: face-placing mode by default, or axis planes / a mode menu via `modes` (see below) |
|
|
241
253
|
| `<top-viewer-measure-length-button>` | Activates length measurement |
|
|
242
254
|
| `<top-viewer-measure-area-button>` | Activates area measurement |
|
|
243
255
|
| `<top-viewer-measure-angle-button>` | Activates angle measurement |
|
|
244
256
|
| `<top-viewer-measure-edge-button>` | Activates edge measurement |
|
|
245
257
|
| `<top-viewer-measure-face-button>` | Activates face measurement |
|
|
246
258
|
| `<top-viewer-measure-volume-button>` | Activates volume measurement |
|
|
259
|
+
| `<top-viewer-measure-button>` | All measurement modes in one button (a menu by default), or a chosen subset via `modes` (see below) |
|
|
247
260
|
| `<top-viewer-projection-toggle-button>` | Toggles Perspective ⇄ Orthographic |
|
|
248
261
|
| `<top-viewer-walkthrough-button>` | Toggles first-person walkthrough |
|
|
262
|
+
| `<top-viewer-navigation-button>` | Switches between orbit and first-person navigation (a menu by default), or a single chosen mode via `modes` (see below) |
|
|
249
263
|
|
|
250
264
|
Each one self-wires from context — drop it anywhere under `<top-viewer>`
|
|
251
265
|
and it works, with no props to pass in.
|
|
266
|
+
|
|
267
|
+
### ✂️ Clip button modes
|
|
268
|
+
|
|
269
|
+
Without any attribute, `<top-viewer-clip-button>` enters face-placing mode:
|
|
270
|
+
double-click a face in the model and a clip plane is dropped on it. The
|
|
271
|
+
optional `modes` attribute changes what the button offers — a whitespace-
|
|
272
|
+
(or comma-) separated list of `x`, `y`, `z` and `face` (case-insensitive;
|
|
273
|
+
unknown tokens are ignored, duplicates dropped, the order you write is the
|
|
274
|
+
order shown):
|
|
275
|
+
|
|
276
|
+
```html
|
|
277
|
+
<!-- a menu with four entries: X axis, Y axis, Z axis, Face -->
|
|
278
|
+
<top-viewer-clip-button modes="x y z face"></top-viewer-clip-button>
|
|
279
|
+
|
|
280
|
+
<!-- one mode = no menu; a click creates a horizontal cut right away -->
|
|
281
|
+
<top-viewer-clip-button modes="y"></top-viewer-clip-button>
|
|
282
|
+
|
|
283
|
+
<!-- default: face-placing mode -->
|
|
284
|
+
<top-viewer-clip-button></top-viewer-clip-button>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- **One mode** — the button runs it directly on click. `face` is the
|
|
288
|
+
default face-placing behavior.
|
|
289
|
+
- **Two or more** — clicking opens a menu with one entry per mode ("X
|
|
290
|
+
axis", "Y axis", "Z axis", "Face"); picking one runs it and closes the
|
|
291
|
+
menu.
|
|
292
|
+
- **Axis modes** create a single plane through the center of the bounding
|
|
293
|
+
box of all loaded models (nothing happens if no model is loaded). Axes
|
|
294
|
+
follow the three.js convention — **Y is vertical**, so `y` is a
|
|
295
|
+
horizontal cut. The plane's normal is the *negative* axis — `(-1, 0, 0)`,
|
|
296
|
+
`(0, -1, 0)`, `(0, 0, -1)` — so the plane clips what lies on the
|
|
297
|
+
positive side of the axis (`y` removes everything above the middle of
|
|
298
|
+
the model). Axis planes behave exactly like face-placed ones: same
|
|
299
|
+
section styling, listed in the objects outliner, draggable by their
|
|
300
|
+
gizmo, removable with Delete.
|
|
301
|
+
- The button shows as active only while face-placing mode is on.
|
|
302
|
+
|
|
303
|
+
### 📏 Measure button modes
|
|
304
|
+
|
|
305
|
+
`<top-viewer-measure-button>` puts the measurement tools behind a single
|
|
306
|
+
button. Without any attribute it offers all six modes — `length`, `area`,
|
|
307
|
+
`angle`, `edge`, `face`, `volume` — in a menu. The optional `modes`
|
|
308
|
+
attribute picks a subset (whitespace- or comma-separated, case-insensitive;
|
|
309
|
+
unknown tokens are ignored, duplicates dropped, your order is kept):
|
|
310
|
+
|
|
311
|
+
```html
|
|
312
|
+
<!-- default: a menu with all six modes -->
|
|
313
|
+
<top-viewer-measure-button></top-viewer-measure-button>
|
|
314
|
+
|
|
315
|
+
<!-- a menu with just these three, in this order -->
|
|
316
|
+
<top-viewer-measure-button modes="length area volume"></top-viewer-measure-button>
|
|
317
|
+
|
|
318
|
+
<!-- one mode = no menu; a click starts that measurement right away -->
|
|
319
|
+
<top-viewer-measure-button modes="length"></top-viewer-measure-button>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
- **One mode** — a click activates it directly, exactly like the matching
|
|
323
|
+
standalone button (`length` and `area` measure freely; `edge` and `face`
|
|
324
|
+
snap to edges and faces).
|
|
325
|
+
- **Two or more** — a click opens a menu with one text-labelled entry per
|
|
326
|
+
mode ("Length", "Area", "Angle", "Edge", "Face", "Volume"); picking one
|
|
327
|
+
activates it and closes the menu.
|
|
328
|
+
- The button shows as active while any of its modes is the active
|
|
329
|
+
measurement mode, and its icon switches to that mode's icon (a ruler
|
|
330
|
+
otherwise). In the menu, the active mode's entry is marked.
|
|
331
|
+
|
|
332
|
+
### 🧭 Navigation button modes
|
|
333
|
+
|
|
334
|
+
`<top-viewer-navigation-button>` switches how the camera moves. Without any
|
|
335
|
+
attribute it offers both modes — `orbit` and `first-person` — in a menu.
|
|
336
|
+
The optional `modes` attribute narrows or reorders them (whitespace- or
|
|
337
|
+
comma-separated, case-insensitive; unknown tokens are ignored, duplicates
|
|
338
|
+
dropped, your order is kept):
|
|
339
|
+
|
|
340
|
+
```html
|
|
341
|
+
<!-- default: a menu with "Orbit" and "First person" -->
|
|
342
|
+
<top-viewer-navigation-button></top-viewer-navigation-button>
|
|
343
|
+
|
|
344
|
+
<!-- one mode = no menu; a click switches straight to it -->
|
|
345
|
+
<top-viewer-navigation-button modes="orbit"></top-viewer-navigation-button>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
- `orbit` turns first-person navigation off (nothing happens if it already
|
|
349
|
+
is); `first-person` turns it on (nothing happens if it already is) — the
|
|
350
|
+
same first-person navigation `<top-viewer-walkthrough-button>` toggles.
|
|
351
|
+
- The button's icon always shows the current mode, and the button is active
|
|
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.
|
|
252
376
|
*/
|