@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.
- package/README.md +59 -46
- package/dist/built-in/index.d.ts +486 -563
- package/dist/cli/lib/npmrc.d.ts +28 -0
- package/dist/cli/lib/npmrc.test.d.ts +1 -0
- package/dist/cli.js +91 -7
- package/dist/core/client.d.ts +10 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +42 -32
- package/dist/types/npm.d.ts +14 -0
- package/dist/vite-env.d.ts +1 -1
- package/docs/builtin/paths.json +74 -70
- package/docs/cli/create.md +28 -28
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/package.json +100 -100
- package/resources/AGENTS.md +40 -40
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/UIManager/src/app/example.ts +99 -99
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
- package/src/built-in/index.ts +649 -755
- package/src/cli/templates/app/README.md +241 -241
- package/src/cli/templates/app/index.html +35 -27
- package/src/cli/templates/app/package.json +31 -27
- package/src/cli/templates/app/src/app.ts +37 -0
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -0
- package/src/cli/templates/app/src/main.ts +340 -56
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
- package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
- package/src/cli/templates/app/src/setups/card-header.ts +35 -0
- package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
- package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
- package/src/cli/templates/app/src/setups/clipper.ts +75 -0
- package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
- package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
- package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
- package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
- package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
- package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
- package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
- package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
- package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
- package/src/cli/templates/app/src/setups/hider.ts +98 -0
- package/src/cli/templates/app/src/setups/index.ts +33 -3
- package/src/cli/templates/app/src/setups/inspection.ts +136 -0
- package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
- package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
- package/src/cli/templates/app/src/setups/measurements.ts +133 -0
- package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
- package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
- package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
- package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
- package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
- package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
- package/src/cli/templates/app/src/setups/styles.ts +375 -0
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
- package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
- package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
- package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
- package/src/cli/templates/app/src/ui-components/index.ts +2 -1
- package/src/cli/templates/app/tsconfig.json +16 -17
- package/src/cli/templates/app/vite.config.js +45 -42
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -4
- package/src/built-in/AppManager/example.ts +0 -86
- package/src/built-in/FileList/example.ts +0 -107
- package/src/built-in/TabbedNavigation/example.ts +0 -115
- package/src/built-in/ViewportsManager/example.ts +0 -106
- package/src/cli/templates/app/src/setups/fragments.ts +0 -18
- package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
- 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
|
+
};
|