@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,528 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import * as OBC from "@thatopen/components";
|
|
3
|
+
import * as OBF from "@thatopen/components-front";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WALKTHROUGH / first-person navigation — a headless CONTROLLER (no UI).
|
|
7
|
+
*
|
|
8
|
+
* FLY mode (no raycasts): WASD / arrow keys move the eye along the view
|
|
9
|
+
* direction — forward/back on the full look vector (so looking up + W ascends),
|
|
10
|
+
* strafe on the camera right — E/Q add explicit world up/down, and LEFT-button
|
|
11
|
+
* drag turns the view. There is NO collision and NO floor-follow/gravity: it's
|
|
12
|
+
* pure free flight (collision can be layered back on later as an option). The
|
|
13
|
+
* orbit anchor (pivot dot) is off for the whole session, and the Hoverer is
|
|
14
|
+
* GATED on stillness — off while moving/looking, on when standing still so you
|
|
15
|
+
* can inspect elements from where you stand. `toggle()` enters/exits; entering
|
|
16
|
+
* seeds the view from the current orbit pose, and exiting KEEPS the walk camera —
|
|
17
|
+
* orbit controls are re-enabled from wherever the user walked to (no snap back).
|
|
18
|
+
* Move speed (and an as-yet unused eye height, kept for the future collision
|
|
19
|
+
* mode) are set via setters.
|
|
20
|
+
*
|
|
21
|
+
* This module owns ONLY the engine + state — it renders no button. It returns a
|
|
22
|
+
* controller so a host (e.g. the bottom action toolbar) can drive it from a
|
|
23
|
+
* single mdi:walk icon and reflect the active state via `onChange`.
|
|
24
|
+
*
|
|
25
|
+
* Movement is frame-rate independent (everything scales by dt). Defaults are in
|
|
26
|
+
* MODEL UNITS (meters-ish); the speed setter adapts to other units.
|
|
27
|
+
*
|
|
28
|
+
* CAMERA DRIVE: drives the LIVE viewport camera. Orbit INPUT is disabled
|
|
29
|
+
* (controls.enabled = false) AND the OBC camera component is paused
|
|
30
|
+
* (camera.enabled = false) so its per-frame controls.update() can't revert us;
|
|
31
|
+
* we then write `world.camera.three` (position + quaternion + matrix) directly
|
|
32
|
+
* each frame — that's what the renderer reads, so movement is immediate.
|
|
33
|
+
* Mouse-look is LEFT-button press-and-drag only (NO pointer lock — the platform
|
|
34
|
+
* iframe is sandboxed without it, and requesting it only logs a "Blocked pointer
|
|
35
|
+
* lock" error). On exit, orbit controls resume FROM the current walk pose (the
|
|
36
|
+
* pivot is seated a short distance ahead of the eye); projection stays perspective.
|
|
37
|
+
*
|
|
38
|
+
* @param components engine components
|
|
39
|
+
* @returns a {@link WalkthroughController}
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface WalkthroughController {
|
|
43
|
+
/** Enter walkthrough if inactive, exit if active. */
|
|
44
|
+
toggle(): void;
|
|
45
|
+
/** Explicitly enter (no-op if already active). */
|
|
46
|
+
enter(): void;
|
|
47
|
+
/** Explicitly exit (no-op if inactive). */
|
|
48
|
+
exit(): void;
|
|
49
|
+
/** Whether walkthrough mode is currently active. */
|
|
50
|
+
isActive(): boolean;
|
|
51
|
+
/** Subscribe to active-state changes. Returns an unsubscribe fn. */
|
|
52
|
+
onChange(listener: (active: boolean) => void): () => void;
|
|
53
|
+
/** Set the eye height (model units, > 0). */
|
|
54
|
+
setEyeHeight(value: number): void;
|
|
55
|
+
/** Set the horizontal move speed (model units/sec, > 0). */
|
|
56
|
+
setSpeed(value: number): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Defaults in model units (assume meters). Eye height + speed are user-adjustable.
|
|
60
|
+
const DEFAULT_EYE = 1.7; // eye height above the floor
|
|
61
|
+
const DEFAULT_SPEED = 3.5; // horizontal move speed, units/sec
|
|
62
|
+
const RUN_MULT = 2.4; // Shift = run
|
|
63
|
+
const EXIT_PIVOT_DIST = 5; // model units ahead to seat the orbit pivot on exit
|
|
64
|
+
const MOUSE_SENS = 0.0022; // radians per pixel of mouse movement
|
|
65
|
+
const PITCH_LIMIT = Math.PI / 2 - 0.05; // clamp look up/down just shy of straight
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
type AnyWorld = any;
|
|
69
|
+
|
|
70
|
+
export const walkthrough = (components: OBC.Components): WalkthroughController => {
|
|
71
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
72
|
+
const hoverer = components.get(OBF.Hoverer);
|
|
73
|
+
|
|
74
|
+
const firstWorld = (): AnyWorld =>
|
|
75
|
+
[...components.get(OBC.Worlds).list.values()][0];
|
|
76
|
+
|
|
77
|
+
// ── State ──────────────────────────────────────────────────────
|
|
78
|
+
let active = false;
|
|
79
|
+
let eyeHeight = DEFAULT_EYE;
|
|
80
|
+
let moveSpeed = DEFAULT_SPEED;
|
|
81
|
+
|
|
82
|
+
// Active-state listeners (host UI reflects the toggle through these).
|
|
83
|
+
const listeners = new Set<(active: boolean) => void>();
|
|
84
|
+
const notify = () => {
|
|
85
|
+
for (const cb of listeners) {
|
|
86
|
+
try {
|
|
87
|
+
cb(active);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn("[walkthrough] onChange listener threw", error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// First-person look (radians). Built into a YXZ euler each frame.
|
|
95
|
+
let yaw = 0;
|
|
96
|
+
let pitch = 0;
|
|
97
|
+
const pos = new THREE.Vector3();
|
|
98
|
+
|
|
99
|
+
const keys = new Set<string>();
|
|
100
|
+
let raf = 0;
|
|
101
|
+
let lastT = 0;
|
|
102
|
+
let lookDirty = false;
|
|
103
|
+
// Latches true on the FIRST real input (move or look). Until then the rendered
|
|
104
|
+
// camera is left EXACTLY as it was on enter — so entering, and spamming the
|
|
105
|
+
// toggle, never nudges/rotates the camera (the per-frame write is gated on it,
|
|
106
|
+
// and exit only re-seats the orbit pivot when the camera was actually driven).
|
|
107
|
+
let touched = false;
|
|
108
|
+
|
|
109
|
+
// The OBC camera component's own `enabled` flag, paused during walkthrough so
|
|
110
|
+
// its per-frame controls.update() can't revert our direct camera writes.
|
|
111
|
+
let savedCamEnabled: boolean | null = null;
|
|
112
|
+
// The Hoverer's original `enabled`. During walkthrough hover is GATED on
|
|
113
|
+
// stillness (off while moving/looking, on when standing still); restored on exit.
|
|
114
|
+
let savedHovererEnabled: boolean | null = null;
|
|
115
|
+
// The world's `dynamicAnchor` (orbit pivot dot), disabled for the whole session
|
|
116
|
+
// so no pivot appears while walking; restored on exit.
|
|
117
|
+
let savedAnchor: boolean | null = null;
|
|
118
|
+
let loggedKey = false; // one-shot: confirm WASD actually reaches the handler
|
|
119
|
+
|
|
120
|
+
// Hover gate: enable the Hoverer only when STILL (no movement keys, not
|
|
121
|
+
// dragging), and only if it was originally enabled. Guarded so we only write +
|
|
122
|
+
// clear on a transition, not every frame.
|
|
123
|
+
const applyHoverGate = (allowHover: boolean) => {
|
|
124
|
+
if (savedHovererEnabled === null) return; // hoverer unavailable
|
|
125
|
+
const want = allowHover ? savedHovererEnabled : false;
|
|
126
|
+
try {
|
|
127
|
+
if (hoverer.enabled !== want) {
|
|
128
|
+
hoverer.enabled = want;
|
|
129
|
+
if (!want) hoverer.clear?.();
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
/* non-fatal */
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Reusable scratch (no per-frame allocation in the hot path).
|
|
137
|
+
const _euler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
138
|
+
const _quat = new THREE.Quaternion();
|
|
139
|
+
const _forward = new THREE.Vector3();
|
|
140
|
+
const _right = new THREE.Vector3();
|
|
141
|
+
const _step = new THREE.Vector3();
|
|
142
|
+
const _WORLD_UP = new THREE.Vector3(0, 1, 0); // E/Q vertical axis
|
|
143
|
+
|
|
144
|
+
// ── Direction vectors from yaw/pitch ───────────────────────────
|
|
145
|
+
// FLY mode: forward is the FULL view direction (includes pitch), so looking up
|
|
146
|
+
// + W ascends and looking down + S descends — true free flight, no raycasts.
|
|
147
|
+
const updateDirs = () => {
|
|
148
|
+
_euler.set(pitch, yaw, 0, "YXZ");
|
|
149
|
+
_quat.setFromEuler(_euler);
|
|
150
|
+
_forward.set(0, 0, -1).applyQuaternion(_quat);
|
|
151
|
+
_right.set(1, 0, 0).applyQuaternion(_quat);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ── Per-frame step ─────────────────────────────────────────────
|
|
155
|
+
const tick = (now: number) => {
|
|
156
|
+
if (!active) return;
|
|
157
|
+
const world = firstWorld();
|
|
158
|
+
const controls = world?.camera?.controls;
|
|
159
|
+
if (!controls) {
|
|
160
|
+
raf = requestAnimationFrame(tick);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const dt = Math.min((now - lastT) / 1000, 0.05); // clamp big gaps (tab switch)
|
|
164
|
+
lastT = now;
|
|
165
|
+
updateDirs();
|
|
166
|
+
|
|
167
|
+
// FLY mode (no raycasts): WASD moves the eye along the view direction —
|
|
168
|
+
// forward/back on _forward (full look dir, so up/down too), strafe on _right.
|
|
169
|
+
// E / Q add explicit WORLD vertical (+Y / −Y) so the user can ascend/descend
|
|
170
|
+
// without relying on pitch+W.
|
|
171
|
+
let mf = 0;
|
|
172
|
+
let mr = 0;
|
|
173
|
+
let mv = 0;
|
|
174
|
+
if (keys.has("w") || keys.has("arrowup")) mf += 1;
|
|
175
|
+
if (keys.has("s") || keys.has("arrowdown")) mf -= 1;
|
|
176
|
+
if (keys.has("d") || keys.has("arrowright")) mr += 1;
|
|
177
|
+
if (keys.has("a") || keys.has("arrowleft")) mr -= 1;
|
|
178
|
+
if (keys.has("e")) mv += 1;
|
|
179
|
+
if (keys.has("q")) mv -= 1;
|
|
180
|
+
let moved = false;
|
|
181
|
+
if (mf !== 0 || mr !== 0 || mv !== 0) {
|
|
182
|
+
const speed = (keys.has("shift") ? moveSpeed * RUN_MULT : moveSpeed) * dt;
|
|
183
|
+
_step
|
|
184
|
+
.set(0, 0, 0)
|
|
185
|
+
.addScaledVector(_forward, mf)
|
|
186
|
+
.addScaledVector(_right, mr)
|
|
187
|
+
.addScaledVector(_WORLD_UP, mv);
|
|
188
|
+
if (_step.lengthSq() > 0) {
|
|
189
|
+
_step.normalize().multiplyScalar(speed);
|
|
190
|
+
pos.add(_step);
|
|
191
|
+
moved = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Hover only when STANDING STILL — off while moving (any WASD/E/Q held) or
|
|
196
|
+
// looking (left-drag). Lets the user inspect elements from where they stand.
|
|
197
|
+
applyHoverGate(mf === 0 && mr === 0 && mv === 0 && !dragging);
|
|
198
|
+
|
|
199
|
+
// Drive the LIVE camera DIRECTLY every frame — but ONLY once the user has
|
|
200
|
+
// actually moved or looked. The OBC camera component's update() is paused
|
|
201
|
+
// (see enter), so camera-controls' update() can't run and revert us — we
|
|
202
|
+
// own the camera. Gating on `touched` means entering (and spamming
|
|
203
|
+
// enter/exit) leaves the rendered pose byte-for-byte untouched: no
|
|
204
|
+
// yaw/pitch round-trip re-orientation, no drift. `_quat` (from yaw/pitch)
|
|
205
|
+
// orients it; pos is the eye. Writing the THREE camera + its world matrix
|
|
206
|
+
// is what the renderer reads, so movement is immediate and never fought.
|
|
207
|
+
if (moved || lookDirty) touched = true;
|
|
208
|
+
lookDirty = false;
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
const cam = world?.camera?.three as any;
|
|
211
|
+
if (cam && touched) {
|
|
212
|
+
cam.position.set(pos.x, pos.y, pos.z);
|
|
213
|
+
cam.quaternion.copy(_quat);
|
|
214
|
+
cam.updateMatrixWorld(true);
|
|
215
|
+
}
|
|
216
|
+
raf = requestAnimationFrame(tick);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// ── Enter / exit ───────────────────────────────────────────────
|
|
220
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
221
|
+
const k = e.key.toLowerCase();
|
|
222
|
+
if (["w", "a", "s", "d", "e", "q", "arrowup", "arrowdown", "arrowleft", "arrowright", "shift"].includes(k)) {
|
|
223
|
+
if (!loggedKey) {
|
|
224
|
+
loggedKey = true;
|
|
225
|
+
console.log("[walkthrough] movement key reached handler:", k); // confirms key capture
|
|
226
|
+
}
|
|
227
|
+
keys.add(k);
|
|
228
|
+
if (k.startsWith("arrow")) e.preventDefault();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const onKeyUp = (e: KeyboardEvent) => keys.delete(e.key.toLowerCase());
|
|
232
|
+
|
|
233
|
+
// Mouse-look: only while a LEFT-button drag is in progress (see pointerdown).
|
|
234
|
+
// movementX/Y are valid during a drag with no pointer lock needed.
|
|
235
|
+
let dragging = false;
|
|
236
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
237
|
+
if (!active || !dragging) return;
|
|
238
|
+
yaw -= e.movementX * MOUSE_SENS;
|
|
239
|
+
pitch -= e.movementY * MOUSE_SENS;
|
|
240
|
+
pitch = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, pitch));
|
|
241
|
+
lookDirty = true;
|
|
242
|
+
};
|
|
243
|
+
// Look = LEFT-button press-and-drag ONLY. We never request pointer lock — the
|
|
244
|
+
// platform sandboxes the viewer in an iframe without `allow-pointer-lock`, so
|
|
245
|
+
// requestPointerLock only logs a "Blocked pointer lock" error. movementX/Y are
|
|
246
|
+
// valid during a drag regardless, so drag-look needs no lock.
|
|
247
|
+
const onCanvasPointerDown = (e: PointerEvent) => {
|
|
248
|
+
if (!active || e.button !== 0) return; // left button only
|
|
249
|
+
dragging = true;
|
|
250
|
+
hideHint();
|
|
251
|
+
};
|
|
252
|
+
const onWindowPointerUp = () => {
|
|
253
|
+
dragging = false;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
let canvas: HTMLCanvasElement | null = null;
|
|
257
|
+
|
|
258
|
+
// ── Hint overlay (discoverability: how to drive walkthrough) ───
|
|
259
|
+
let hintEl: HTMLElement | null = null;
|
|
260
|
+
let hintTimer: number | undefined;
|
|
261
|
+
const hideHint = () => {
|
|
262
|
+
if (hintTimer !== undefined) {
|
|
263
|
+
clearTimeout(hintTimer);
|
|
264
|
+
hintTimer = undefined;
|
|
265
|
+
}
|
|
266
|
+
if (hintEl) {
|
|
267
|
+
hintEl.remove();
|
|
268
|
+
hintEl = null;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const showHint = () => {
|
|
272
|
+
if (!canvas) return;
|
|
273
|
+
const host = canvas.parentElement;
|
|
274
|
+
if (!host) return;
|
|
275
|
+
hideHint();
|
|
276
|
+
const el = document.createElement("div");
|
|
277
|
+
el.textContent = "Left-drag to look · WASD move · E/Q up/down · click Walk to exit";
|
|
278
|
+
el.style.cssText = [
|
|
279
|
+
"position: absolute",
|
|
280
|
+
"left: 50%",
|
|
281
|
+
"bottom: 4.5rem",
|
|
282
|
+
"transform: translateX(-50%)",
|
|
283
|
+
"z-index: 30",
|
|
284
|
+
"pointer-events: none",
|
|
285
|
+
"padding: 0.35rem 0.7rem",
|
|
286
|
+
"font-size: 0.75rem",
|
|
287
|
+
"white-space: nowrap",
|
|
288
|
+
"color: var(--bim-ui_bg-contrast-100, #e3e3e3)",
|
|
289
|
+
"background: rgba(20,20,24,0.85)",
|
|
290
|
+
"border: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.2))",
|
|
291
|
+
"border-radius: var(--bim-ui_size-2xs, 0.375rem)",
|
|
292
|
+
"box-shadow: 0 2px 10px rgba(0,0,0,0.35)",
|
|
293
|
+
].join(";");
|
|
294
|
+
if (getComputedStyle(host).position === "static") host.style.position = "relative";
|
|
295
|
+
host.appendChild(el);
|
|
296
|
+
hintEl = el;
|
|
297
|
+
hintTimer = window.setTimeout(hideHint, 4500);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const enter = () => {
|
|
301
|
+
const world = firstWorld();
|
|
302
|
+
const controls = world?.camera?.controls;
|
|
303
|
+
const projection = world?.camera?.projection;
|
|
304
|
+
canvas = world?.renderer?.three?.domElement ?? null;
|
|
305
|
+
if (!controls || !canvas) {
|
|
306
|
+
console.warn("[walkthrough] no world camera/canvas to drive");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log(
|
|
310
|
+
"[walkthrough] enter — driving live camera",
|
|
311
|
+
world?.camera?.three?.type,
|
|
312
|
+
"| controls bound to rendered camera; loop starting",
|
|
313
|
+
);
|
|
314
|
+
// Seed the first-person view from the ACTUAL rendered camera transform (world
|
|
315
|
+
// position + quaternion-forward), NOT controls.getPosition/getTarget — so the
|
|
316
|
+
// view doesn't move at all on enter (no eye-height reset, no re-orient). Same
|
|
317
|
+
// ground-truth source exit uses, so enter and exit are symmetric.
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
319
|
+
const cam = world?.camera?.three as any;
|
|
320
|
+
const p = new THREE.Vector3();
|
|
321
|
+
if (cam) {
|
|
322
|
+
// Force the world matrix current first — reading a stale matrixWorld (e.g.
|
|
323
|
+
// mid-frame right after the previous exit's setLookAt) would seed a pose
|
|
324
|
+
// that's slightly off, and repeated enter/exit would compound it.
|
|
325
|
+
cam.updateMatrixWorld(true);
|
|
326
|
+
cam.getWorldPosition(p);
|
|
327
|
+
_forward.set(0, 0, -1).applyQuaternion(cam.quaternion).normalize();
|
|
328
|
+
} else {
|
|
329
|
+
controls.getPosition(p);
|
|
330
|
+
const t = new THREE.Vector3();
|
|
331
|
+
controls.getTarget(t);
|
|
332
|
+
_forward.copy(t).sub(p).normalize();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// First-person must be perspective. (Only an Orthographic orbit re-projects on
|
|
336
|
+
// enter; a Perspective orbit keeps the exact same view.)
|
|
337
|
+
if (projection?.current === "Orthographic") void projection.set?.("Perspective");
|
|
338
|
+
|
|
339
|
+
// Reconstruct yaw/pitch from the live forward → the first tick re-applies the
|
|
340
|
+
// identical orientation (orbit cameras have no roll, so this is exact).
|
|
341
|
+
yaw = Math.atan2(_forward.x, -_forward.z);
|
|
342
|
+
pitch = Math.asin(Math.max(-1, Math.min(1, _forward.y)));
|
|
343
|
+
pos.copy(p);
|
|
344
|
+
// FLY mode: enter exactly at the current eye point — no floor snap.
|
|
345
|
+
|
|
346
|
+
controls.enabled = false; // stop orbit INPUT
|
|
347
|
+
// Pause the OBC camera component's per-frame update so its controls.update()
|
|
348
|
+
// can't re-apply the orbit pose and revert our direct camera writes.
|
|
349
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
350
|
+
const camComp = world?.camera as any;
|
|
351
|
+
// Snapshot the Hoverer's enabled state; the per-frame gate (applyHoverGate)
|
|
352
|
+
// then toggles it by stillness. Clear any stuck overlay on entry.
|
|
353
|
+
try {
|
|
354
|
+
savedHovererEnabled = typeof hoverer?.enabled === "boolean" ? hoverer.enabled : null;
|
|
355
|
+
hoverer?.clear?.();
|
|
356
|
+
} catch {
|
|
357
|
+
/* hoverer not ready — non-fatal */
|
|
358
|
+
}
|
|
359
|
+
// Disable the orbit anchor (pivot dot) for the whole walkthrough — no pivot
|
|
360
|
+
// should appear while walking. Restored on exit.
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
362
|
+
const w = world as any;
|
|
363
|
+
if (w && typeof w.dynamicAnchor === "boolean") {
|
|
364
|
+
savedAnchor = w.dynamicAnchor;
|
|
365
|
+
w.dynamicAnchor = false;
|
|
366
|
+
}
|
|
367
|
+
savedCamEnabled = typeof camComp?.enabled === "boolean" ? camComp.enabled : null;
|
|
368
|
+
if (savedCamEnabled !== null) camComp.enabled = false;
|
|
369
|
+
active = true;
|
|
370
|
+
dragging = false;
|
|
371
|
+
loggedKey = false;
|
|
372
|
+
// Start "untouched": the first tick must NOT write the camera (that would
|
|
373
|
+
// re-orient via the lossy yaw/pitch round-trip). Only real input sets it.
|
|
374
|
+
touched = false;
|
|
375
|
+
lookDirty = false;
|
|
376
|
+
window.addEventListener("keydown", onKeyDown);
|
|
377
|
+
window.addEventListener("keyup", onKeyUp);
|
|
378
|
+
window.addEventListener("pointerup", onWindowPointerUp);
|
|
379
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
380
|
+
canvas.addEventListener("pointerdown", onCanvasPointerDown);
|
|
381
|
+
// Start the movement loop + notify. Look is press-and-drag only — no
|
|
382
|
+
// pointer-lock request (the sandboxed iframe blocks it and only logs an
|
|
383
|
+
// error); WASD fly + drag-look need no lock.
|
|
384
|
+
lastT = performance.now();
|
|
385
|
+
raf = requestAnimationFrame(tick);
|
|
386
|
+
notify();
|
|
387
|
+
showHint();
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const exit = () => {
|
|
391
|
+
if (!active) return;
|
|
392
|
+
active = false;
|
|
393
|
+
dragging = false;
|
|
394
|
+
keys.clear();
|
|
395
|
+
hideHint();
|
|
396
|
+
if (raf) cancelAnimationFrame(raf);
|
|
397
|
+
raf = 0;
|
|
398
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
399
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
400
|
+
window.removeEventListener("pointerup", onWindowPointerUp);
|
|
401
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
402
|
+
canvas?.removeEventListener("pointerdown", onCanvasPointerDown);
|
|
403
|
+
|
|
404
|
+
const world = firstWorld();
|
|
405
|
+
const controls = world?.camera?.controls;
|
|
406
|
+
// Re-enable the OBC camera component so its update loop (and orbit) resumes.
|
|
407
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
408
|
+
const camComp = world?.camera as any;
|
|
409
|
+
if (savedCamEnabled !== null && camComp) camComp.enabled = savedCamEnabled;
|
|
410
|
+
savedCamEnabled = null;
|
|
411
|
+
// Restore the Hoverer's original enabled state and the orbit anchor.
|
|
412
|
+
try {
|
|
413
|
+
if (savedHovererEnabled !== null) hoverer.enabled = savedHovererEnabled;
|
|
414
|
+
} catch {
|
|
415
|
+
/* non-fatal */
|
|
416
|
+
}
|
|
417
|
+
savedHovererEnabled = null;
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
419
|
+
const w = world as any;
|
|
420
|
+
if (w && savedAnchor !== null) w.dynamicAnchor = savedAnchor;
|
|
421
|
+
savedAnchor = null;
|
|
422
|
+
if (controls) {
|
|
423
|
+
controls.enabled = true;
|
|
424
|
+
}
|
|
425
|
+
// Only re-seat the orbit pivot if the user ACTUALLY drove the camera this
|
|
426
|
+
// session. If nothing was touched (e.g. spamming the toggle), the camera is
|
|
427
|
+
// exactly where orbit left it, its target is still valid, and re-posing would
|
|
428
|
+
// only risk nudging it — so we leave it completely alone (true no-op exit).
|
|
429
|
+
if (controls && touched) {
|
|
430
|
+
// KEEP the walk camera: re-seed orbit controls FROM the ACTUAL rendered
|
|
431
|
+
// first-person transform (ground truth — not our yaw/pitch state, in case
|
|
432
|
+
// they ever diverge), with the orbit pivot a short distance ahead along the
|
|
433
|
+
// look direction. No snap-back to the pre-walk pose, no re-frame. Stays in
|
|
434
|
+
// perspective (the walk projection).
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
436
|
+
const cam = world?.camera?.three as any;
|
|
437
|
+
const eye = new THREE.Vector3();
|
|
438
|
+
const fwd = new THREE.Vector3(0, 0, -1);
|
|
439
|
+
if (cam) {
|
|
440
|
+
cam.getWorldPosition(eye);
|
|
441
|
+
fwd.applyQuaternion(cam.quaternion).normalize();
|
|
442
|
+
} else {
|
|
443
|
+
updateDirs();
|
|
444
|
+
eye.copy(pos);
|
|
445
|
+
fwd.copy(_forward);
|
|
446
|
+
}
|
|
447
|
+
// The orbit pivot sits `dist` ahead along the look dir. CRITICAL: the eye↔
|
|
448
|
+
// target distance must stay within the controls' min/max, or setLookAt CLAMPS
|
|
449
|
+
// it by MOVING the eye (the intermittent exit jump — only when dolly limits
|
|
450
|
+
// are active). Clamp the pivot distance into [minDistance, maxDistance] so the
|
|
451
|
+
// resting distance is valid and the eye stays EXACTLY where the user stood.
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
|
+
const cc = controls as any;
|
|
454
|
+
const minD = typeof cc.minDistance === "number" ? cc.minDistance : 0;
|
|
455
|
+
const maxD = typeof cc.maxDistance === "number" ? cc.maxDistance : Infinity;
|
|
456
|
+
let dist = EXIT_PIVOT_DIST;
|
|
457
|
+
if (Number.isFinite(minD) && dist < minD) dist = minD;
|
|
458
|
+
if (Number.isFinite(maxD) && dist > maxD) dist = maxD;
|
|
459
|
+
const setPose = () =>
|
|
460
|
+
controls.setLookAt(
|
|
461
|
+
eye.x, eye.y, eye.z,
|
|
462
|
+
eye.x + fwd.x * dist,
|
|
463
|
+
eye.y + fwd.y * dist,
|
|
464
|
+
eye.z + fwd.z * dist,
|
|
465
|
+
false,
|
|
466
|
+
);
|
|
467
|
+
void setPose();
|
|
468
|
+
// Apply immediately so the next render is already at the walk pose...
|
|
469
|
+
try {
|
|
470
|
+
controls.update(0);
|
|
471
|
+
} catch {
|
|
472
|
+
/* some controls builds reject delta 0 — harmless */
|
|
473
|
+
}
|
|
474
|
+
// ...and guard against any engine system that reframes the camera on the
|
|
475
|
+
// orbit→resume transition (auto-anchor / aspect / fit). Re-assert the pose
|
|
476
|
+
// for a few frames; stop as soon as it holds. Logs once if it ever drifts,
|
|
477
|
+
// so a persistent reframer is visible instead of silently winning.
|
|
478
|
+
if (cam) {
|
|
479
|
+
let tries = 0;
|
|
480
|
+
const hold = () => {
|
|
481
|
+
if (active) return; // a new walkthrough started — stop
|
|
482
|
+
const cur = new THREE.Vector3();
|
|
483
|
+
cam.getWorldPosition(cur);
|
|
484
|
+
if (cur.distanceToSquared(eye) > 1e-6) {
|
|
485
|
+
if (tries === 0) {
|
|
486
|
+
console.log("[walkthrough] exit pose drifted — re-asserting", cur.distanceTo(eye));
|
|
487
|
+
}
|
|
488
|
+
void setPose();
|
|
489
|
+
// Cap tight: the transition reframe happens in the first frame(s);
|
|
490
|
+
// a low cap avoids fighting a fast user orbit started right after.
|
|
491
|
+
if (++tries < 3) requestAnimationFrame(hold);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
requestAnimationFrame(hold);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
notify();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const toggle = () => (active ? exit() : enter());
|
|
501
|
+
|
|
502
|
+
// A model unload while walking → bail out safely (geometry it stood on is gone).
|
|
503
|
+
fragments.list.onItemDeleted.add(() => {
|
|
504
|
+
if (active) exit();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// ── Controller (no UI) ─────────────────────────────────────────
|
|
508
|
+
return {
|
|
509
|
+
toggle,
|
|
510
|
+
enter: () => {
|
|
511
|
+
if (!active) enter();
|
|
512
|
+
},
|
|
513
|
+
exit: () => {
|
|
514
|
+
if (active) exit();
|
|
515
|
+
},
|
|
516
|
+
isActive: () => active,
|
|
517
|
+
onChange: (listener) => {
|
|
518
|
+
listeners.add(listener);
|
|
519
|
+
return () => listeners.delete(listener);
|
|
520
|
+
},
|
|
521
|
+
setEyeHeight: (value) => {
|
|
522
|
+
if (Number.isFinite(value) && value > 0) eyeHeight = value;
|
|
523
|
+
},
|
|
524
|
+
setSpeed: (value) => {
|
|
525
|
+
if (Number.isFinite(value) && value > 0) moveSpeed = value;
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as BUI from "@thatopen/ui";
|
|
2
|
+
import { AppInfoSectionComponent } from "./src";
|
|
3
|
+
import { getAppManager } from "../../app";
|
|
4
|
+
|
|
5
|
+
export const appInfoSectionTemplate: AppInfoSectionComponent = ({
|
|
6
|
+
components,
|
|
7
|
+
}) => {
|
|
8
|
+
const app = getAppManager(components);
|
|
9
|
+
|
|
10
|
+
const fileItems =
|
|
11
|
+
app.projectData?.files.map(
|
|
12
|
+
(f: { name: string }) => BUI.html`<bim-label>${f.name}</bim-label>`,
|
|
13
|
+
) ?? [];
|
|
14
|
+
|
|
15
|
+
return BUI.html`
|
|
16
|
+
<bim-panel-section label="App Info" icon="solar:info-circle-bold">
|
|
17
|
+
<bim-label>App ID: ${app.client?.context.appId}</bim-label>
|
|
18
|
+
<bim-label>Project ID: ${app.client?.context.projectId}</bim-label>
|
|
19
|
+
<bim-label>API URL: ${app.client?.context.apiUrl}</bim-label>
|
|
20
|
+
<bim-label>Files: ${fileItems.length}</bim-label>
|
|
21
|
+
${fileItems}
|
|
22
|
+
</bim-panel-section>
|
|
23
|
+
`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export * from "./src";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./types";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as BUI from "@thatopen/ui";
|
|
2
|
+
import * as OBC from "@thatopen/components";
|
|
3
|
+
|
|
4
|
+
export interface AppInfoSectionState {
|
|
5
|
+
components: OBC.Components;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type AppInfoSectionComponent =
|
|
9
|
+
BUI.StatefullComponent<AppInfoSectionState>;
|
|
10
|
+
|
|
11
|
+
// Used in App type (app.ts) to type this element slot in the grid.
|
|
12
|
+
export type AppInfoSectionGridElement = {
|
|
13
|
+
name: "appInfoSection";
|
|
14
|
+
state: AppInfoSectionState;
|
|
15
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as BUI from "@thatopen/ui";
|
|
2
|
+
import { CloudRunner } from "../../bim-components";
|
|
3
|
+
import { CloudRunnerSectionComponent } from "./src";
|
|
4
|
+
|
|
5
|
+
export const cloudRunnerSectionTemplate: CloudRunnerSectionComponent = ({
|
|
6
|
+
components,
|
|
7
|
+
}) => {
|
|
8
|
+
const runner = components.get(CloudRunner);
|
|
9
|
+
|
|
10
|
+
const messageItems = runner.messages.map(
|
|
11
|
+
(m) => BUI.html`<bim-label>${m}</bim-label>`,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
return BUI.html`
|
|
15
|
+
<bim-panel-section label="Cloud Component" icon="solar:code-bold">
|
|
16
|
+
<bim-label>Component ID: ${runner.componentId}</bim-label>
|
|
17
|
+
<bim-label>Local server: ${runner.localServerUrl}</bim-label>
|
|
18
|
+
<div style="display:flex;gap:0.5rem;">
|
|
19
|
+
<bim-button
|
|
20
|
+
label="Run Local"
|
|
21
|
+
icon="solar:play-bold"
|
|
22
|
+
@click=${() => runner.run(true)}
|
|
23
|
+
></bim-button>
|
|
24
|
+
<bim-button
|
|
25
|
+
label="Run Deployed"
|
|
26
|
+
icon="solar:cloud-bold-duotone"
|
|
27
|
+
@click=${() => runner.run(false)}
|
|
28
|
+
></bim-button>
|
|
29
|
+
</div>
|
|
30
|
+
<bim-label>${runner.status}</bim-label>
|
|
31
|
+
<bim-label>Progress: ${runner.progress}%</bim-label>
|
|
32
|
+
${messageItems}
|
|
33
|
+
</bim-panel-section>
|
|
34
|
+
`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export * from "./src";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./types";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as OBC from "@thatopen/components";
|
|
2
|
+
import * as BUI from "@thatopen/ui";
|
|
3
|
+
|
|
4
|
+
export interface CloudRunnerSectionState {
|
|
5
|
+
components: OBC.Components;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CloudRunnerSectionComponent =
|
|
9
|
+
BUI.StatefullComponent<CloudRunnerSectionState>;
|
|
10
|
+
|
|
11
|
+
export type CloudRunnerSectionGridElement = {
|
|
12
|
+
name: "cloudRunnerSection";
|
|
13
|
+
state: CloudRunnerSectionState;
|
|
14
|
+
};
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./app-info-section";
|
|
2
|
+
export * from "./cloud-runner-section";
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
-
"skipLibCheck": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"moduleResolution": "bundler",
|
|
9
|
-
"allowImportingTsExtensions": true,
|
|
10
|
-
"resolveJsonModule": true,
|
|
11
|
-
"isolatedModules": true,
|
|
12
|
-
"noEmit": true,
|
|
13
|
-
"strict": true
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|