@thatopen/services 0.3.11 → 0.3.13

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.
Files changed (38) hide show
  1. package/dist/cli/commands/revit.d.ts +9 -0
  2. package/dist/cli/lib/revit-addin.d.ts +20 -0
  3. package/dist/cli.js +128 -0
  4. package/dist/cloud/LasToPotree.d.ts +59 -0
  5. package/dist/cloud/SplatToFq.d.ts +38 -0
  6. package/dist/cloud/index.d.ts +2 -0
  7. package/docs/builtin/paths.json +86 -86
  8. package/docs/cloud/GaussianSplatConverter.md +43 -0
  9. package/docs/cloud/PointCloudConverter.md +53 -0
  10. package/docs/revit-collab-quickstart.md +205 -0
  11. package/package.json +95 -95
  12. package/src/built-in/ClashesManager/example.ts +283 -283
  13. package/src/built-in/UIManager/src/clashes-panel/example.ts +160 -160
  14. package/src/built-in/UIManager/src/models-list/example.ts +122 -118
  15. package/src/cli/templates/app/package.json +33 -33
  16. package/src/cli/templates/app/src/setups/camera-tools.ts +0 -63
  17. package/src/cli/templates/app/src/setups/camera-views.ts +0 -201
  18. package/src/cli/templates/app/src/setups/card-header.ts +0 -35
  19. package/src/cli/templates/app/src/setups/clipper-tool.ts +0 -773
  20. package/src/cli/templates/app/src/setups/clipper.ts +0 -75
  21. package/src/cli/templates/app/src/setups/cloud-runner.ts +0 -13
  22. package/src/cli/templates/app/src/setups/exploded-view.ts +0 -246
  23. package/src/cli/templates/app/src/setups/helper-panel.ts +0 -88
  24. package/src/cli/templates/app/src/setups/hider.ts +0 -98
  25. package/src/cli/templates/app/src/setups/index.ts +0 -23
  26. package/src/cli/templates/app/src/setups/inspection.ts +0 -136
  27. package/src/cli/templates/app/src/setups/measurement-panel.ts +0 -188
  28. package/src/cli/templates/app/src/setups/measurement-tool.ts +0 -588
  29. package/src/cli/templates/app/src/setups/measurements.ts +0 -133
  30. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +0 -316
  31. package/src/cli/templates/app/src/setups/plans-panel.ts +0 -619
  32. package/src/cli/templates/app/src/setups/right-sidebar.ts +0 -178
  33. package/src/cli/templates/app/src/setups/styles-panel.ts +0 -110
  34. package/src/cli/templates/app/src/setups/styles.ts +0 -375
  35. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +0 -156
  36. package/src/cli/templates/app/src/setups/tool-mode.ts +0 -23
  37. package/src/cli/templates/app/src/setups/toolbar.ts +0 -229
  38. package/src/cli/templates/app/src/setups/ui-manager.ts +0 -27
@@ -1,133 +0,0 @@
1
- import * as THREE from "three";
2
- import * as OBC from "@thatopen/components";
3
- import * as OBF from "@thatopen/components-front";
4
- import type { ModeTool } from "./tool-mode";
5
-
6
- // Brand purple, matching the selection outline so measurements read as "ours".
7
- const MEASURE_COLOR = 0x6528d7;
8
-
9
- const canvasOf = (world: OBC.World): HTMLCanvasElement => {
10
- const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
11
- .domElement;
12
- if (!canvas) {
13
- throw new Error("measurements setup: world has no renderer with a canvas");
14
- }
15
- return canvas;
16
- };
17
-
18
- /**
19
- * Length-measurement tool. Wraps `OBF.LengthMeasurement` as a toolbar mode tool:
20
- *
21
- * - double-click places measurement points (snapping to vertices/edges); a
22
- * dimension line + label is drawn between them,
23
- * - Escape cancels the in-progress measurement (handled by the component),
24
- * - Delete / Backspace → delete the measurement under the cursor,
25
- * - Shift + Delete / Shift + Backspace → clear ALL measurements.
26
- *
27
- * Finished measurements persist when the mode is exited.
28
- */
29
- export const lengthMeasurement = (
30
- components: OBC.Components,
31
- world: OBC.World,
32
- ): ModeTool => {
33
- const measurer = components.get(OBF.LengthMeasurement);
34
- measurer.world = world;
35
- measurer.color = new THREE.Color(MEASURE_COLOR);
36
- const canvas = canvasOf(world);
37
-
38
- const onDblClick = () => measurer.create();
39
-
40
- const onKeyDown = (event: KeyboardEvent) => {
41
- if (event.code !== "Delete" && event.code !== "Backspace") return;
42
- if (event.shiftKey) {
43
- measurer.list.clear();
44
- } else {
45
- void measurer.delete();
46
- }
47
- };
48
-
49
- let active = false;
50
-
51
- return {
52
- label: "Measure length",
53
- icon: "mdi:ruler",
54
- activate() {
55
- if (active) return;
56
- active = true;
57
- measurer.enabled = true;
58
- canvas.addEventListener("dblclick", onDblClick);
59
- window.addEventListener("keydown", onKeyDown);
60
- canvas.style.cursor = "crosshair";
61
- },
62
- deactivate() {
63
- if (!active) return;
64
- active = false;
65
- measurer.cancelCreation(); // drop any half-finished measurement
66
- measurer.enabled = false;
67
- canvas.removeEventListener("dblclick", onDblClick);
68
- window.removeEventListener("keydown", onKeyDown);
69
- canvas.style.cursor = "";
70
- },
71
- };
72
- };
73
-
74
- /**
75
- * Area-measurement tool. Wraps `OBF.AreaMeasurement` as a toolbar mode tool:
76
- *
77
- * - double-click adds polygon points; Enter closes/finalizes the polygon and
78
- * shows the filled area + value,
79
- * - Escape cancels the in-progress polygon (handled by the component),
80
- * - Delete / Backspace → delete the area under the cursor,
81
- * - Shift + Delete / Shift + Backspace → clear ALL areas.
82
- *
83
- * NOTE: the click-to-add-point vs. double-click-to-create flow is worth a live
84
- * check; tuned here to mirror the length tool's double-click entry.
85
- */
86
- export const areaMeasurement = (
87
- components: OBC.Components,
88
- world: OBC.World,
89
- ): ModeTool => {
90
- const measurer = components.get(OBF.AreaMeasurement);
91
- measurer.world = world;
92
- measurer.color = new THREE.Color(MEASURE_COLOR);
93
- const canvas = canvasOf(world);
94
-
95
- const onDblClick = () => measurer.create();
96
-
97
- const onKeyDown = (event: KeyboardEvent) => {
98
- if (event.code === "Enter" || event.code === "NumpadEnter") {
99
- measurer.endCreation();
100
- return;
101
- }
102
- if (event.code !== "Delete" && event.code !== "Backspace") return;
103
- if (event.shiftKey) {
104
- measurer.list.clear();
105
- } else {
106
- void measurer.delete();
107
- }
108
- };
109
-
110
- let active = false;
111
-
112
- return {
113
- label: "Measure area",
114
- icon: "mdi:vector-square",
115
- activate() {
116
- if (active) return;
117
- active = true;
118
- measurer.enabled = true;
119
- canvas.addEventListener("dblclick", onDblClick);
120
- window.addEventListener("keydown", onKeyDown);
121
- canvas.style.cursor = "crosshair";
122
- },
123
- deactivate() {
124
- if (!active) return;
125
- active = false;
126
- measurer.cancelCreation();
127
- measurer.enabled = false;
128
- canvas.removeEventListener("dblclick", onDblClick);
129
- window.removeEventListener("keydown", onKeyDown);
130
- canvas.style.cursor = "";
131
- },
132
- };
133
- };
@@ -1,316 +0,0 @@
1
- import * as THREE from "three";
2
- import * as OBC from "@thatopen/components";
3
-
4
- /**
5
- * NAVIGATION GIZMO — a Blender/Spline-style axis-ball orientation widget pinned
6
- * TOP-RIGHT of the viewport. Replaces the old camera-views overlay.
7
- *
8
- * A small rounded dark DISC contains a soft axis triad that reflects the LIVE
9
- * camera orientation (it rotates with the camera): a neutral hub and three muted
10
- * axes (soft red/green/blue), each a ball on a SHORT, THICK rounded bar. No
11
- * labels, no outer ring. Colour is FIXED BY AXIS SIGN — +X/+Y/+Z are coloured
12
- * (ball + arm); −X/−Y/−Z are small flat-gray dots (no arm), regardless of facing.
13
- * Click a ball → orient + frame the model's union bbox in a SINGLE animated move
14
- * (one `setLookAt` whose distance is computed to frame the sphere — no separate
15
- * fitToSphere). Hovering an end gently brightens it. No home/fit button (the
16
- * Focus command zoom-to-fits the whole model when nothing is selected) and no
17
- * ortho/persp toggle (the bottom toolbar owns it).
18
- *
19
- * Same orient-on-click technique as a view-cube; this is the axis-ball variant.
20
- * The widget renders in its OWN tiny WebGL renderer (a fixed ~60px disc overlay,
21
- * tight around the triad) fully decoupled from the main viewport — a constant
22
- * on-screen size. The gizmo camera is ALWAYS ORTHOGRAPHIC (axonometric — the arms
23
- * never foreshorten), posed each frame to mirror the main camera's orientation,
24
- * so the end facing you is the axis the camera is looking down. Picking is a
25
- * single raycast against the six ball meshes → the nearest ball's axis direction.
26
- *
27
- * Self-mounts into the viewport overlay (like the old camera-views). Wire from
28
- * main.ts with one line, after the viewport exists:
29
- *
30
- * navigationGizmo(components, viewerElement);
31
- *
32
- * @param components engine components
33
- * @param container optional viewport element to overlay; defaults to the first
34
- * world's renderer container.
35
- */
36
-
37
- const SIZE = 60; // px, fixed on-screen widget size (~50% smaller)
38
- const DIST = 5; // gizmo camera distance from the triad
39
- const FRUSTUM = 1.08; // ortho half-extent — tight around the triad (little padding)
40
- const R = 0.82; // ball distance from the hub
41
-
42
- // Colour is FIXED BY AXIS SIGN: +X/+Y/+Z are coloured (ball + arm), the negatives
43
- // are always flat gray — independent of facing.
44
- const AXES: { dir: [number, number, number]; color: number; positive: boolean }[] = [
45
- { dir: [1, 0, 0], color: 0xcf6f6f, positive: true }, // +X soft red
46
- { dir: [-1, 0, 0], color: 0xcf6f6f, positive: false },
47
- { dir: [0, 1, 0], color: 0x86b572, positive: true }, // +Y soft green
48
- { dir: [0, -1, 0], color: 0x86b572, positive: false },
49
- { dir: [0, 0, 1], color: 0x7095c9, positive: true }, // +Z soft blue
50
- { dir: [0, 0, -1], color: 0x7095c9, positive: false },
51
- ];
52
- const GRAY = new THREE.Color(0x6e7177); // negatives are always flat gray
53
-
54
- interface Axis {
55
- ball: THREE.Mesh<THREE.SphereGeometry, THREE.MeshBasicMaterial>;
56
- arm: THREE.Mesh<THREE.CapsuleGeometry, THREE.MeshBasicMaterial>;
57
- baseColor: THREE.Color; // fixed appearance colour (axis colour, or gray for −axes)
58
- baseScale: number;
59
- dir: THREE.Vector3;
60
- }
61
-
62
- export const navigationGizmo = (
63
- components: OBC.Components,
64
- container?: HTMLElement,
65
- ) => {
66
- const fragments = components.get(OBC.FragmentsManager);
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- const firstWorld = (): any => [...components.get(OBC.Worlds).list.values()][0];
69
-
70
- // ── Overlay DOM (pinned top-right, click-through except on the widget) ──
71
- const root = document.createElement("div");
72
- root.className = "nav-gizmo";
73
- root.style.cssText = [
74
- "position: absolute",
75
- "top: 1rem",
76
- "right: 1rem",
77
- "z-index: 20",
78
- "display: flex",
79
- "flex-direction: column",
80
- "align-items: center",
81
- "gap: 0.25rem",
82
- "pointer-events: none",
83
- ].join(";");
84
-
85
- // The subtle dark DISC is the canvas itself (the alpha renderer clears
86
- // transparent, so this soft CSS background + round clip shows through). No bold
87
- // outer ring — just the disc + a faint shadow for separation.
88
- const canvas = document.createElement("canvas");
89
- canvas.style.cssText = [
90
- `width:${SIZE}px`,
91
- `height:${SIZE}px`,
92
- "pointer-events:auto",
93
- "cursor:pointer",
94
- "border-radius:50%",
95
- // Opaque circular background matching the panels (BUI theme base colour).
96
- "background:var(--bim-ui_bg-base, var(--bim-ui_bg-contrast-20, #1b1b1f))",
97
- "box-shadow:0 1px 6px rgba(0,0,0,0.28)",
98
- ].join(";");
99
- root.appendChild(canvas);
100
-
101
- // ── Gizmo scene (own renderer, fully decoupled from the main viewport) ──
102
- const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
103
- renderer.setPixelRatio(window.devicePixelRatio || 1);
104
- renderer.setSize(SIZE, SIZE, false);
105
- renderer.outputColorSpace = THREE.SRGBColorSpace;
106
-
107
- const scene = new THREE.Scene();
108
- const gizmoCam = new THREE.OrthographicCamera(
109
- -FRUSTUM, FRUSTUM, FRUSTUM, -FRUSTUM, 0.1, 100,
110
- );
111
-
112
- // Small neutral center hub.
113
- scene.add(
114
- new THREE.Mesh(
115
- new THREE.SphereGeometry(0.06, 16, 12),
116
- new THREE.MeshBasicMaterial({ color: 0x8a8f96 }),
117
- ),
118
- );
119
-
120
- // Six axis ends: a ball + a SHORT, THICK rounded bar (capsule) from the hub.
121
- // Appearance is FIXED BY SIGN: +axes coloured (ball + arm), −axes flat gray
122
- // (small ball, no arm).
123
- const BALL_R = 0.12; // ball radius
124
- const ARM_R = 0.05; // arm (capsule) radius — fat
125
- const ARM_INNER = 0.08; // bar starts just outside the hub
126
- const ARM_OUTER = R - BALL_R; // bar ends just inside the ball
127
- const ARM_LEN = ARM_OUTER - ARM_INNER - 2 * ARM_R; // cylinder length (caps add 2·R)
128
- const ARM_MID = (ARM_INNER + ARM_OUTER) / 2; // centre offset along the axis
129
- const _yAxis = new THREE.Vector3(0, 1, 0);
130
- const axes: Axis[] = [];
131
- for (const a of AXES) {
132
- const dir = new THREE.Vector3(...a.dir);
133
- const baseColor = a.positive ? new THREE.Color(a.color) : GRAY.clone();
134
- const baseScale = 1; // negatives are full-size too — just gray + no arm
135
-
136
- const ball = new THREE.Mesh(
137
- new THREE.SphereGeometry(BALL_R, 20, 16),
138
- new THREE.MeshBasicMaterial({ color: baseColor.clone(), transparent: true, opacity: a.positive ? 1 : 0.7 }),
139
- );
140
- ball.position.copy(dir).multiplyScalar(R);
141
- ball.scale.setScalar(baseScale);
142
- ball.renderOrder = 2;
143
- scene.add(ball);
144
-
145
- // Capsule points along +Y by default → rotate to the axis, centre it on the
146
- // hub→ball midpoint. Only the coloured (+) axes show an arm.
147
- const arm = new THREE.Mesh(
148
- new THREE.CapsuleGeometry(ARM_R, ARM_LEN, 6, 12),
149
- new THREE.MeshBasicMaterial({ color: baseColor.clone(), transparent: true }),
150
- );
151
- arm.quaternion.setFromUnitVectors(_yAxis, dir);
152
- arm.position.copy(dir).multiplyScalar(ARM_MID);
153
- arm.renderOrder = 1;
154
- arm.visible = a.positive;
155
- scene.add(arm);
156
-
157
- axes.push({ ball, arm, dir, baseColor, baseScale });
158
- }
159
-
160
- // ── Pose the gizmo camera to mirror the main camera each frame ──
161
- const _fwd = new THREE.Vector3();
162
- const _up = new THREE.Vector3();
163
- const _lastQuat = new THREE.Quaternion(0, 0, 0, 0);
164
- const poseGizmo = (): boolean => {
165
- const main = firstWorld()?.camera?.three as THREE.Camera | undefined;
166
- if (!main) return false;
167
- main.getWorldDirection(_fwd); // direction the main camera looks (into scene)
168
- gizmoCam.position.copy(_fwd).multiplyScalar(-DIST); // mirror that viewpoint
169
- _up.set(0, 1, 0).applyQuaternion(main.quaternion); // honor camera roll
170
- gizmoCam.up.copy(_up);
171
- gizmoCam.lookAt(0, 0, 0);
172
- return true;
173
- };
174
-
175
- const WHITE = new THREE.Color(0xffffff);
176
- let hovered: Axis | null = null;
177
-
178
- let hoverDirty = false;
179
- const render = () => {
180
- if (!poseGizmo()) return;
181
- renderer.render(scene, gizmoCam);
182
- };
183
-
184
- // Render only when the orientation changed (or a hover changed) — cheap.
185
- let raf = 0;
186
- const loop = () => {
187
- raf = requestAnimationFrame(loop);
188
- void raf;
189
- const main = firstWorld()?.camera?.three as THREE.Camera | undefined;
190
- if (!main) return;
191
- if (hoverDirty || !main.quaternion.equals(_lastQuat)) {
192
- _lastQuat.copy(main.quaternion);
193
- hoverDirty = false;
194
- render();
195
- }
196
- };
197
-
198
- // ── Picking: nearest ball under the cursor ─────────────────────
199
- const raycaster = new THREE.Raycaster();
200
- const ndc = new THREE.Vector2();
201
- const ballMeshes = axes.map((ax) => ax.ball);
202
- const pickBall = (ev: PointerEvent | MouseEvent): Axis | null => {
203
- const rect = canvas.getBoundingClientRect();
204
- ndc.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
205
- ndc.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
206
- raycaster.setFromCamera(ndc, gizmoCam);
207
- const hit = raycaster.intersectObjects(ballMeshes, false)[0];
208
- if (!hit) return null;
209
- return axes.find((ax) => ax.ball === hit.object) ?? null;
210
- };
211
-
212
- const setHover = (ax: Axis | null) => {
213
- if (hovered === ax) return;
214
- if (hovered) {
215
- hovered.ball.scale.setScalar(hovered.baseScale);
216
- hovered.ball.material.color.copy(hovered.baseColor);
217
- }
218
- hovered = ax;
219
- if (ax) {
220
- ax.ball.scale.setScalar(ax.baseScale * 1.18); // subtle grow
221
- ax.ball.material.color.copy(ax.baseColor).lerp(WHITE, 0.22); // gentle brighten
222
- }
223
- canvas.style.cursor = ax ? "pointer" : "default";
224
- hoverDirty = true;
225
- };
226
-
227
- // ── Orient + frame in ONE animated move ────────────────────────
228
- // Compute the framing distance ourselves and issue a SINGLE setLookAt (which
229
- // tweens position + target together) — no separate fitToSphere, so a gizmo
230
- // click is one continuous orient-and-frame motion, not two.
231
- const unionBox = () => {
232
- const box = new THREE.Box3();
233
- for (const model of fragments.list.values()) {
234
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
- const b = (model as any).box as THREE.Box3 | undefined;
236
- if (b && !b.isEmpty()) box.union(b);
237
- }
238
- return box;
239
- };
240
-
241
- const orientTo = (dir: THREE.Vector3) => {
242
- const world = firstWorld();
243
- const controls = world?.camera?.controls;
244
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
- const main = world?.camera?.three as any;
246
- if (!controls?.setLookAt || !main) return;
247
- try {
248
- const box = unionBox();
249
- if (box.isEmpty()) return; // nothing loaded → no-op gracefully
250
- // Frame the actual MODEL AABB (not its bounding sphere — the sphere over-
251
- // estimates ~1.7× for a box, landing the camera too far). For an axis view
252
- // the distance is driven by the box's two PERPENDICULAR extents (fit the
253
- // larger against the narrower fov half-angle) plus half the depth along the
254
- // axis so the camera sits just outside the box. ONE animated setLookAt.
255
- const v = dir.clone().normalize();
256
- const center = box.getCenter(new THREE.Vector3());
257
- const size = box.getSize(new THREE.Vector3());
258
- const dims = [size.x, size.y, size.z];
259
- const ax = Math.abs(v.x) > 0.5 ? 0 : Math.abs(v.y) > 0.5 ? 1 : 2; // dominant axis
260
- const depth = dims[ax] || 1;
261
- const perp = dims.filter((_, i) => i !== ax);
262
- const maxPerp = Math.max(perp[0] || 0, perp[1] || 0, 1e-3);
263
- if (main.isPerspectiveCamera) {
264
- const vHalf = THREE.MathUtils.degToRad(main.fov || 50) / 2;
265
- const aspect = main.aspect || 1;
266
- const hHalf = Math.atan(Math.tan(vHalf) * aspect);
267
- const fit = Math.max(0.001, Math.min(vHalf, hHalf));
268
- const d = (maxPerp / 2 / Math.tan(fit) + depth / 2) * 1.12;
269
- const eye = v.multiplyScalar(d).add(center);
270
- void controls.setLookAt(eye.x, eye.y, eye.z, center.x, center.y, center.z, true);
271
- } else {
272
- // Orthographic: distance is cosmetic; place outside the box and tween the
273
- // ortho zoom CONCURRENTLY (same frame → one smooth motion) to fit it.
274
- const d = depth / 2 + maxPerp * 2;
275
- const eye = v.multiplyScalar(d).add(center);
276
- void controls.setLookAt(eye.x, eye.y, eye.z, center.x, center.y, center.z, true);
277
- const fw = main.right - main.left;
278
- const fh = main.top - main.bottom;
279
- const zoom = Math.min(fw, fh) / (maxPerp * 1.12);
280
- if (typeof controls.zoomTo === "function" && Number.isFinite(zoom) && zoom > 0) {
281
- void controls.zoomTo(zoom, true);
282
- }
283
- }
284
- } catch (error) {
285
- console.warn("[navigation-gizmo] orient failed", error);
286
- }
287
- };
288
-
289
- // ── Interaction ────────────────────────────────────────────────
290
- canvas.addEventListener("pointermove", (ev) => setHover(pickBall(ev)));
291
- canvas.addEventListener("pointerleave", () => setHover(null));
292
- canvas.addEventListener("click", (ev) => {
293
- const b = pickBall(ev);
294
- if (b) void orientTo(b.dir);
295
- });
296
-
297
- // ── Mount + start ──────────────────────────────────────────────
298
- const resolveContainer = (): HTMLElement | undefined => {
299
- if (container) return container;
300
- const world = firstWorld();
301
- const canvasEl = world?.renderer?.three?.domElement as HTMLElement | undefined;
302
- return (canvasEl?.parentElement as HTMLElement | undefined) ?? undefined;
303
- };
304
- const host = resolveContainer();
305
- if (host) {
306
- if (getComputedStyle(host).position === "static") host.style.position = "relative";
307
- host.appendChild(root);
308
- } else {
309
- console.warn("[navigation-gizmo] no viewport found to overlay; append the returned element manually");
310
- }
311
-
312
- render(); // initial paint
313
- loop();
314
-
315
- return root;
316
- };