@thatopen/services 0.1.3 → 0.3.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.
Files changed (120) hide show
  1. package/README.md +59 -46
  2. package/dist/built-in/index.d.ts +486 -563
  3. package/dist/cli/lib/npmrc.d.ts +28 -0
  4. package/dist/cli/lib/npmrc.test.d.ts +1 -0
  5. package/dist/cli.js +91 -7
  6. package/dist/core/client.d.ts +10 -0
  7. package/dist/index.cjs.js +2 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.es.js +42 -32
  10. package/dist/types/npm.d.ts +14 -0
  11. package/dist/vite-env.d.ts +1 -1
  12. package/docs/builtin/paths.json +74 -70
  13. package/docs/cli/create.md +28 -28
  14. package/docs/cli/local-server.md +38 -38
  15. package/docs/cli/login.md +32 -32
  16. package/docs/cli/publish.md +31 -31
  17. package/docs/cli/run.md +26 -26
  18. package/docs/cli/serve.md +43 -43
  19. package/docs/cli/swap.md +30 -30
  20. package/package.json +100 -100
  21. package/resources/AGENTS.md +40 -40
  22. package/src/built-in/CDEManager/example.ts +214 -214
  23. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  24. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  25. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  26. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  27. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  28. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  29. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  30. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  31. package/src/built-in/UIManager/src/app/example.ts +99 -99
  32. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
  33. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
  34. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
  35. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
  36. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
  37. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
  38. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
  39. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
  40. package/src/built-in/index.ts +649 -755
  41. package/src/cli/templates/app/README.md +241 -241
  42. package/src/cli/templates/app/index.html +35 -27
  43. package/src/cli/templates/app/package.json +31 -27
  44. package/src/cli/templates/app/src/app.ts +37 -0
  45. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  48. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  49. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  50. package/src/cli/templates/app/src/globals.ts +1 -0
  51. package/src/cli/templates/app/src/main.ts +340 -56
  52. package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
  53. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
  54. package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
  55. package/src/cli/templates/app/src/setups/card-header.ts +35 -0
  56. package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
  57. package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
  58. package/src/cli/templates/app/src/setups/clipper.ts +75 -0
  59. package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
  60. package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
  61. package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
  62. package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
  63. package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
  64. package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
  65. package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
  66. package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
  67. package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
  68. package/src/cli/templates/app/src/setups/hider.ts +98 -0
  69. package/src/cli/templates/app/src/setups/index.ts +33 -3
  70. package/src/cli/templates/app/src/setups/inspection.ts +136 -0
  71. package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
  72. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
  73. package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
  74. package/src/cli/templates/app/src/setups/measurements.ts +133 -0
  75. package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
  76. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
  77. package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
  78. package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
  79. package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
  80. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
  81. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
  82. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
  83. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
  84. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
  85. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
  86. package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
  87. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
  88. package/src/cli/templates/app/src/setups/styles.ts +375 -0
  89. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
  90. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
  91. package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
  92. package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
  93. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
  94. package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
  95. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
  96. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
  97. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
  98. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
  99. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  100. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  101. package/src/cli/templates/app/src/ui-components/index.ts +2 -1
  102. package/src/cli/templates/app/tsconfig.json +16 -17
  103. package/src/cli/templates/app/vite.config.js +45 -42
  104. package/src/cli/templates/cloud-component/README.md +205 -205
  105. package/src/cli/templates/cloud-component/_thatopen +5 -5
  106. package/src/cli/templates/cloud-component/declarations.json +4 -4
  107. package/src/cli/templates/cloud-component/package.json +21 -21
  108. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  109. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  110. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  111. package/src/cli/templates/shared/AGENTS.md +8 -8
  112. package/src/cli/templates/shared/CLAUDE.md +1 -1
  113. package/src/cli/templates/shared/_gitignore +5 -4
  114. package/src/built-in/AppManager/example.ts +0 -86
  115. package/src/built-in/FileList/example.ts +0 -107
  116. package/src/built-in/TabbedNavigation/example.ts +0 -115
  117. package/src/built-in/ViewportsManager/example.ts +0 -106
  118. package/src/cli/templates/app/src/setups/fragments.ts +0 -18
  119. package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
  120. package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
@@ -0,0 +1,316 @@
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
+ };
@@ -0,0 +1,127 @@
1
+ import * as BUI from "@thatopen/ui";
2
+ import type { InspectionInstances, InstanceRow } from "./inspection";
3
+ import { toolPlaceholderUri } from "../assets/tool-placeholder";
4
+ import { cardHeader } from "./card-header";
5
+
6
+ /**
7
+ * OBJECTS outliner panel (UI-reorg increment b). Lists every created clip plane
8
+ * and measurement from the unified {@link InspectionInstances} API, each row with
9
+ * hide/show, enable/disable (clip planes only) and delete. Re-renders on the
10
+ * inspection `onChanged` event (the row actions themselves fire it via the tools,
11
+ * so handlers don't re-render manually). Returns its `bim-panel` WITHOUT
12
+ * self-mounting — main.ts docks it in the activity-bar "Objects" layout.
13
+ *
14
+ * @param inspection unified clip-plane + measurement instance list (from W1)
15
+ * @returns the `bim-panel` element
16
+ */
17
+ const iconFor = (r: InstanceRow): string => {
18
+ if (r.kind === "clip") return "mdi:scissors-cutting";
19
+ switch (r.type) {
20
+ case "Length": return "mdi:ruler";
21
+ case "Area": return "mdi:vector-square";
22
+ case "Angle": return "mdi:angle-acute";
23
+ default: return "mdi:cube-outline";
24
+ }
25
+ };
26
+
27
+ export const objectsPanel = (inspection: InspectionInstances) => {
28
+ // bim-tooltip portals itself into a body-level container while shown and only
29
+ // hides on its parent button's `mouseleave`. A row action (delete / toggle)
30
+ // fires inspection.onChanged → the row re-renders and the button is removed —
31
+ // so the parent never sees `mouseleave` and the portaled tooltip is left
32
+ // orphaned on screen. Firing `mouseleave` on the button first runs the
33
+ // tooltip's hide (re-parents + clears it) BEFORE the row removes, so no stale
34
+ // tooltip lingers after delete/toggle.
35
+ const dismissTip = (e: Event) =>
36
+ (e.currentTarget as HTMLElement | null)?.dispatchEvent(new MouseEvent("mouseleave"));
37
+
38
+ const rowHtml = (r: InstanceRow) => BUI.html`
39
+ <div class="ob-row">
40
+ <bim-icon class="ob-ico" icon=${iconFor(r)}></bim-icon>
41
+ <span class="ob-lbl" title=${r.label}>${r.label}</span>
42
+ <bim-button
43
+ class="ob-act"
44
+ icon=${r.visible ? "mdi:eye-outline" : "mdi:eye-off-outline"}
45
+ @click=${(e: Event) => { dismissTip(e); r.setVisible(!r.visible); }}
46
+ ><bim-tooltip>${r.visible ? "Hide" : "Show"}</bim-tooltip></bim-button>
47
+ ${r.enabled !== undefined
48
+ ? BUI.html`<bim-button
49
+ class="ob-act"
50
+ icon=${r.enabled ? "mdi:toggle-switch-outline" : "mdi:toggle-switch-off-outline"}
51
+ @click=${(e: Event) => { dismissTip(e); r.setEnabled?.(!r.enabled); }}
52
+ ><bim-tooltip>${r.enabled ? "Disable cut" : "Enable cut"}</bim-tooltip></bim-button>`
53
+ : null}
54
+ <bim-button
55
+ class="ob-act"
56
+ icon="mdi:trash-can-outline"
57
+ @click=${(e: Event) => { dismissTip(e); r.remove(); }}
58
+ ><bim-tooltip>Delete</bim-tooltip></bim-button>
59
+ </div>`;
60
+
61
+ // A titled group ("Clip planes" / "Measurements") when its rows are present.
62
+ const group = (title: string, rows: InstanceRow[]) =>
63
+ rows.length === 0
64
+ ? null
65
+ : BUI.html`
66
+ <div class="ob-grp">${title}</div>
67
+ ${rows.map(rowHtml)}`;
68
+
69
+ const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
70
+ // Arity >= 1 (state param) is REQUIRED — an arity-0 callback makes
71
+ // Component.create return a single element, not [el, update].
72
+ (_s) => {
73
+ const rows = inspection.list();
74
+ const clips = rows.filter((r) => r.kind === "clip");
75
+ const measures = rows.filter((r) => r.kind === "measurement");
76
+ return BUI.html`
77
+ <bim-panel
78
+ label="Objects"
79
+ icon="mdi:cube-outline"
80
+ header-hidden
81
+ style="width: 100%; height: 100%; pointer-events: auto;"
82
+ >
83
+ <style>
84
+ .ob-scroll { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
85
+ overflow-y: auto; padding: 0.4rem 0 0.6rem; }
86
+ .ob-scroll::-webkit-scrollbar { width: 0.4rem; }
87
+ .ob-scroll::-webkit-scrollbar-thumb { border-radius: 0.25rem;
88
+ background-color: var(--bim-scrollbar--c, #3C3C41); }
89
+ .ob-grp { padding: 0.45rem 0.75rem 0.2rem; font-size: 0.68rem;
90
+ font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
91
+ color: var(--bim-ui_bg-contrast-60, #99a0ae); }
92
+ .ob-row { display: flex; align-items: center; gap: 0.4rem;
93
+ padding: 0.1rem 0.5rem 0.1rem 0.75rem; min-height: 1.7rem; }
94
+ .ob-row:hover { background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.06)); }
95
+ .ob-ico { flex: 0 0 auto; font-size: 0.9rem;
96
+ color: var(--bim-ui_bg-contrast-60, #99a0ae); }
97
+ .ob-lbl { flex: 1 1 auto; min-width: 0; overflow: hidden;
98
+ text-overflow: ellipsis; white-space: nowrap; font-size: 0.74rem;
99
+ color: var(--bim-ui_bg-contrast-100, #e3e3e3); }
100
+ .ob-act { flex: 0 0 auto; }
101
+ /* Shared empty-state look (same svg placeholder + faded label as the
102
+ Properties panel) so all panels read consistently when empty. */
103
+ .ob-empty { display: flex; flex-direction: column; align-items: center;
104
+ justify-content: center; gap: 0.75rem; height: 100%; min-height: 12rem;
105
+ padding: 1rem; text-align: center; }
106
+ </style>
107
+ <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
108
+ ${cardHeader("mdi:cube-outline", "Objects", "1.1rem")}
109
+ <div class="ob-scroll">
110
+ ${rows.length === 0
111
+ ? BUI.html`<div class="ob-empty">
112
+ <img src=${toolPlaceholderUri} alt="" style="width: 7.5rem; height: auto; opacity: 0.9;" />
113
+ <bim-label style="opacity: 0.55; white-space: normal;">No clip planes or measurements yet. Create them from the Inspection toolbar.</bim-label>
114
+ </div>`
115
+ : BUI.html`${group("Clip planes", clips)}${group("Measurements", measures)}`}
116
+ </div>
117
+ </div>
118
+ </bim-panel>`;
119
+ },
120
+ { tick: 0 },
121
+ );
122
+
123
+ // Re-render whenever an instance is added/removed or its state changes.
124
+ inspection.onChanged.add(() => update({ tick: 0 }));
125
+
126
+ return panel;
127
+ };