@needle-tools/engine 4.4.6 → 4.5.0-alpha

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 (71) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/{needle-engine.bundle-7919deac.light.umd.cjs → needle-engine.bundle-032ef70a.light.umd.cjs} +137 -137
  3. package/dist/{needle-engine.bundle-bd6721d2.min.js → needle-engine.bundle-19ca6713.min.js} +158 -158
  4. package/dist/{needle-engine.bundle-cb4d28f2.umd.cjs → needle-engine.bundle-8bee539f.umd.cjs} +146 -146
  5. package/dist/{needle-engine.bundle-0359367a.js → needle-engine.bundle-916d62ca.js} +2993 -2961
  6. package/dist/{needle-engine.bundle-545dcd43.light.js → needle-engine.bundle-a2aadea9.light.js} +2993 -2961
  7. package/dist/{needle-engine.bundle-381aaeb9.light.min.js → needle-engine.bundle-ff2e699c.light.min.js} +158 -158
  8. package/dist/needle-engine.js +538 -539
  9. package/dist/needle-engine.light.js +538 -539
  10. package/dist/needle-engine.light.min.js +1 -1
  11. package/dist/needle-engine.light.umd.cjs +1 -1
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/lib/engine/api.d.ts +4 -4
  15. package/lib/engine/api.js +4 -4
  16. package/lib/engine/api.js.map +1 -1
  17. package/lib/engine/engine_application.d.ts +5 -0
  18. package/lib/engine/engine_application.js +11 -11
  19. package/lib/engine/engine_application.js.map +1 -1
  20. package/lib/engine/engine_context.js +9 -2
  21. package/lib/engine/engine_context.js.map +1 -1
  22. package/lib/engine/engine_lifecycle_api.d.ts +2 -1
  23. package/lib/engine/engine_lifecycle_api.js +2 -1
  24. package/lib/engine/engine_lifecycle_api.js.map +1 -1
  25. package/lib/engine/webcomponents/buttons.d.ts +15 -3
  26. package/lib/engine/webcomponents/buttons.js +32 -6
  27. package/lib/engine/webcomponents/buttons.js.map +1 -1
  28. package/lib/engine/webcomponents/needle-engine.ar-overlay.d.ts +21 -0
  29. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +167 -0
  30. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -0
  31. package/lib/engine/webcomponents/needle-engine.attributes.d.ts +72 -0
  32. package/lib/engine/webcomponents/needle-engine.attributes.js +2 -0
  33. package/lib/engine/webcomponents/needle-engine.attributes.js.map +1 -0
  34. package/lib/engine/webcomponents/needle-engine.d.ts +113 -0
  35. package/lib/engine/webcomponents/needle-engine.extras.d.ts +6 -0
  36. package/lib/engine/webcomponents/needle-engine.extras.js +14 -0
  37. package/lib/engine/webcomponents/needle-engine.extras.js.map +1 -0
  38. package/lib/engine/webcomponents/needle-engine.js +832 -0
  39. package/lib/engine/webcomponents/needle-engine.js.map +1 -0
  40. package/lib/engine/webcomponents/needle-engine.loading.d.ts +44 -0
  41. package/lib/engine/webcomponents/needle-engine.loading.js +350 -0
  42. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -0
  43. package/lib/engine/xr/NeedleXRSession.js +21 -0
  44. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  45. package/lib/engine-components/CameraUtils.js.map +1 -1
  46. package/lib/engine-components/SceneSwitcher.js +1 -1
  47. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  48. package/lib/engine-components/Skybox.js +1 -1
  49. package/lib/engine-components/Skybox.js.map +1 -1
  50. package/lib/engine-components/ui/EventSystem.js +1 -0
  51. package/lib/engine-components/ui/EventSystem.js.map +1 -1
  52. package/lib/needle-engine.d.ts +1 -1
  53. package/lib/needle-engine.js +1 -1
  54. package/lib/needle-engine.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/engine/api.ts +4 -4
  57. package/src/engine/engine_application.ts +11 -11
  58. package/src/engine/engine_context.ts +11 -2
  59. package/src/engine/engine_lifecycle_api.ts +2 -1
  60. package/src/engine/webcomponents/buttons.ts +37 -8
  61. package/src/engine/{engine_element_overlay.ts → webcomponents/needle-engine.ar-overlay.ts} +2 -2
  62. package/src/engine/{engine_element_extras.ts → webcomponents/needle-engine.extras.ts} +1 -1
  63. package/src/engine/{engine_element_loading.ts → webcomponents/needle-engine.loading.ts} +6 -6
  64. package/src/engine/{engine_element.ts → webcomponents/needle-engine.ts} +15 -16
  65. package/src/engine/xr/NeedleXRSession.ts +26 -2
  66. package/src/engine-components/CameraUtils.ts +1 -1
  67. package/src/engine-components/SceneSwitcher.ts +1 -1
  68. package/src/engine-components/Skybox.ts +1 -1
  69. package/src/engine-components/ui/EventSystem.ts +1 -0
  70. package/src/needle-engine.ts +1 -1
  71. /package/src/engine/{engine_element_attributes.ts → webcomponents/needle-engine.attributes.ts} +0 -0
@@ -0,0 +1,832 @@
1
+ import { AgXToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping } from "three";
2
+ import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
3
+ import { PUBLIC_KEY, VERSION } from "../engine_constants.js";
4
+ import { registerLoader } from "../engine_gltf.js";
5
+ import { hasCommercialLicense } from "../engine_license.js";
6
+ import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "../engine_loaders.js";
7
+ import { NeedleLoader } from "../engine_scenetools.js";
8
+ import { Context } from "../engine_setup.js";
9
+ import { getParam } from "../engine_utils.js";
10
+ import { RGBAColor } from "../js-extensions/RGBAColor.js";
11
+ import { ensureFonts } from "./fonts.js";
12
+ import { arContainerClassName, AROverlayHandler } from "./needle-engine.ar-overlay.js";
13
+ import { calculateProgress01, EngineLoadingView } from "./needle-engine.loading.js";
14
+ // registering loader here too to make sure it's imported when using engine via vanilla js
15
+ registerLoader(NeedleLoader);
16
+ const debug = getParam("debugwebcomponent");
17
+ const htmlTagName = "needle-engine";
18
+ const vrContainerClassName = "vr";
19
+ const desktopContainerClassname = "desktop";
20
+ const knownClasses = [arContainerClassName, vrContainerClassName, desktopContainerClassname];
21
+ const arSessionActiveClassName = "ar-session-active";
22
+ const desktopSessionActiveClassName = "desktop-session-active";
23
+ const observedAttributes = [
24
+ "public-key",
25
+ "version",
26
+ "hash",
27
+ "src",
28
+ "camera-controls",
29
+ "loadstart",
30
+ "progress",
31
+ "loadfinished",
32
+ "dracoDecoderPath",
33
+ "dracoDecoderType",
34
+ "ktx2DecoderPath",
35
+ "tone-mapping",
36
+ "tone-mapping-exposure",
37
+ "background-blurriness",
38
+ "background-color",
39
+ ];
40
+ // https://developers.google.com/web/fundamentals/web-components/customelements
41
+ /**
42
+ * <needle-engine> web component. See {@link NeedleEngineAttributes} attributes for supported attributes
43
+ * The needle engine web component creates and manages a needle engine context which is responsible for rendering a 3D scene using threejs.
44
+ * The needle engine context is created when the src attribute is set and disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
45
+ * The needle engine context is accessible via the context property on the needle engine element (e.g. document.querySelector("needle-engine").context).
46
+ * @link https://engine.needle.tools/docs/reference/needle-engine-attributes
47
+ *
48
+ * @example
49
+ * <needle-engine src="https://example.com/scene.glb"></needle-engine>
50
+ * @example
51
+ * <needle-engine src="https://example.com/scene.glb" camera-controls="false"></needle-engine>
52
+ */
53
+ export class NeedleEngineHTMLElement extends HTMLElement {
54
+ static get observedAttributes() {
55
+ return observedAttributes;
56
+ }
57
+ get loadingProgress01() { return this._loadingProgress01; }
58
+ get loadingFinished() { return this.loadingProgress01 > .999; }
59
+ /**
60
+ * If set to false the camera controls are disabled. Default is true.
61
+ * @type {boolean | null}
62
+ * @memberof NeedleEngineAttributes
63
+ * @example
64
+ * <needle-engine camera-controls="false"></needle-engine>
65
+ * @example
66
+ * <needle-engine camera-controls="true"></needle-engine>
67
+ * @example
68
+ * <needle-engine camera-controls></needle-engine>
69
+ * @example
70
+ * <needle-engine></needle-engine>
71
+ * @returns {boolean | null} if the attribute is not set it returns null
72
+ */
73
+ get cameraControls() {
74
+ const attr = this.getAttribute("camera-controls");
75
+ if (attr == null)
76
+ return null;
77
+ if (attr === null || attr === "False" || attr === "false" || attr === "0" || attr === "none")
78
+ return false;
79
+ return true;
80
+ }
81
+ /**
82
+ * Get the current context for this web component instance. The context is created when the src attribute is set and the loading has finished.
83
+ * The context is disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
84
+ * @returns {Promise<Context>} a promise that resolves to the context when the loading has finished
85
+ */
86
+ getContext() {
87
+ return new Promise((res, _rej) => {
88
+ if (this._context && this.loadingFinished) {
89
+ res(this._context);
90
+ }
91
+ else {
92
+ const cb = () => {
93
+ this.removeEventListener("loadfinished", cb);
94
+ if (this._context && this.loadingFinished) {
95
+ res(this._context);
96
+ }
97
+ };
98
+ this.addEventListener("loadfinished", cb);
99
+ }
100
+ });
101
+ }
102
+ /**
103
+ * Get the context that is created when the src attribute is set and the loading has finished.
104
+ */
105
+ get context() { return this._context; }
106
+ _context;
107
+ _overlay_ar;
108
+ _loadingProgress01 = 0;
109
+ _loadingView;
110
+ _previousSrc = null;
111
+ /** set to true after <needle-engine> did load completely at least once. Set to false when <needle-engine> is removed from the document */
112
+ _didFullyLoad = false;
113
+ constructor() {
114
+ super();
115
+ this._overlay_ar = new AROverlayHandler();
116
+ // TODO: do we want to rename this event?
117
+ this.addEventListener("ready", this.onReady);
118
+ ensureFonts();
119
+ this.attachShadow({ mode: 'open' });
120
+ const template = document.createElement('template');
121
+ template.innerHTML = `<style>
122
+ @import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap');
123
+
124
+ :host {
125
+ position: absolute;
126
+ display: block;
127
+ width: max(600px, 100%);
128
+ height: max(300px, 100%);
129
+ touch-action: none;
130
+
131
+ -webkit-tap-highlight-color: transparent;
132
+ }
133
+
134
+ @media (max-width: 600px) {
135
+ :host {
136
+ width: 100%;
137
+ }
138
+ }
139
+ @media (max-height: 300px) {
140
+ :host {
141
+ height: 100%;
142
+ }
143
+ }
144
+
145
+ :host > div.canvas-wrapper {
146
+ width: 100%;
147
+ height: 100%;
148
+ }
149
+
150
+ :host canvas {
151
+ position: absolute;
152
+ user-select: none;
153
+ -webkit-user-select: none;
154
+
155
+ /** allow touch panning but no pinch zoom **/
156
+ /** but this doesnt work yet:
157
+ * touch-action: pan-x, pan-y;
158
+ **/
159
+
160
+ -webkit-touch-callout: none;
161
+ -webkit-user-drag: none;
162
+ -webkit-user-modify: none;
163
+
164
+ left: 0;
165
+ top: 0;
166
+ }
167
+ :host .content {
168
+ position: absolute;
169
+ top: 0;
170
+ width: 100%;
171
+ height: 100%;
172
+ visibility: visible;
173
+ z-index: 500; /* < must be less than the webxr buttons element */
174
+ pointer-events: none;
175
+ }
176
+ :host .overlay-content {
177
+ position: absolute;
178
+ user-select: auto;
179
+ pointer-events: all;
180
+ }
181
+ :host slot[name="quit-ar"]:hover {
182
+ cursor: pointer;
183
+ }
184
+ :host .quit-ar-button {
185
+ position: absolute;
186
+ // top: env(titlebar-area-y); /** this doesnt work **/
187
+ top: 60px; /** camera access needs a bit more space **/
188
+ right: 20px;
189
+ z-index: 9999;
190
+ }
191
+ </style>
192
+ <div class="canvas-wrapper"> <!-- this wrapper is necessary for WebXR https://github.com/meta-quest/immersive-web-emulator/issues/55 -->
193
+ <canvas></canvas>
194
+ </div>
195
+ <div class="content">
196
+ <slot class="overlay-content"></slot>
197
+ </div>
198
+ `;
199
+ if (this.shadowRoot)
200
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
201
+ this._context = new Context({ domElement: this });
202
+ this.addEventListener("error", this.onError);
203
+ }
204
+ /**
205
+ * @internal
206
+ */
207
+ async connectedCallback() {
208
+ if (debug) {
209
+ console.log("<needle-engine> connected");
210
+ }
211
+ this.setPublicKey();
212
+ this.setVersion();
213
+ this.addEventListener("xr-session-started", this.onXRSessionStarted);
214
+ this.onSetupDesktop();
215
+ if (!this.getAttribute("src")) {
216
+ const global = globalThis["needle:codegen_files"];
217
+ if (debug)
218
+ console.log("src is null, trying to load from globalThis[\"needle:codegen_files\"]", global);
219
+ if (global) {
220
+ if (debug)
221
+ console.log("globalThis[\"needle:codegen_files\"]", global);
222
+ this.setAttribute("src", global);
223
+ }
224
+ }
225
+ if (debug)
226
+ console.log("src", this.getAttribute("src"));
227
+ // we have to wait because codegen does set the src attribute when it's loaded
228
+ // which might happen after the element is connected
229
+ // if the `src` is then still null we want to initialize the default scene
230
+ const loadId = this._loadId;
231
+ setTimeout(() => {
232
+ if (this.isConnected === false)
233
+ return;
234
+ if (loadId !== this._loadId)
235
+ return;
236
+ this.onLoad();
237
+ }, 1);
238
+ }
239
+ /**
240
+ * @internal
241
+ */
242
+ disconnectedCallback() {
243
+ this.removeEventListener("xr-session-started", this.onXRSessionStarted);
244
+ this._didFullyLoad = false;
245
+ const keepAlive = this.getAttribute("keep-alive");
246
+ const dispose = keepAlive == undefined || (keepAlive?.length > 0 && keepAlive !== "true" && keepAlive !== "1");
247
+ if (debug)
248
+ console.warn("<needle-engine> disconnected, keep-alive: \"" + keepAlive + "\"", typeof keepAlive, "Dispose=", dispose);
249
+ if (dispose) {
250
+ if (debug)
251
+ console.warn("<needle-engine> dispose");
252
+ this._context?.dispose();
253
+ this._context = null;
254
+ this._lastSourceFiles = null;
255
+ this._loadId += 1;
256
+ }
257
+ else {
258
+ if (debug)
259
+ console.warn("<needle-engine> is not disposed because keep-alive is set");
260
+ }
261
+ }
262
+ /**
263
+ * @internal
264
+ */
265
+ attributeChangedCallback(name, oldValue, newValue) {
266
+ if (debug)
267
+ console.log("attributeChangedCallback", name, oldValue, newValue);
268
+ switch (name) {
269
+ case "src":
270
+ if (debug)
271
+ console.warn("<needle-engine src>\nchanged from \"", oldValue, "\" to \"", newValue, "\"");
272
+ this.onLoad();
273
+ // this._watcher?.onSourceChanged(newValue);
274
+ break;
275
+ case "hash":
276
+ if (this._context) {
277
+ this._context.hash = newValue;
278
+ }
279
+ break;
280
+ case "loadstart":
281
+ case "progress":
282
+ case "loadfinished":
283
+ if (typeof newValue === "string" && newValue.length > 0) {
284
+ if (debug)
285
+ console.log(name + " attribute changed", newValue);
286
+ this.registerEventFromAttribute(name, newValue);
287
+ }
288
+ break;
289
+ case "dracoDecoderPath":
290
+ if (debug)
291
+ console.log("dracoDecoderPath", newValue);
292
+ setDracoDecoderPath(newValue);
293
+ break;
294
+ case "dracoDecoderType":
295
+ if (newValue === "wasm" || newValue === "js") {
296
+ if (debug)
297
+ console.log("dracoDecoderType", newValue);
298
+ setDracoDecoderType(newValue);
299
+ }
300
+ else
301
+ console.error("Invalid dracoDecoderType", newValue, "expected js or wasm");
302
+ break;
303
+ case "ktx2DecoderPath":
304
+ if (debug)
305
+ console.log("ktx2DecoderPath", newValue);
306
+ setKtx2TranscoderPath(newValue);
307
+ break;
308
+ case "tone-mapping": {
309
+ this.applyAttributes();
310
+ break;
311
+ }
312
+ case "tone-mapping-exposure": {
313
+ this.applyAttributes();
314
+ break;
315
+ }
316
+ case "background-blurriness": {
317
+ const value = parseFloat(newValue);
318
+ if (value != undefined && this._context) {
319
+ this._context.scene.backgroundBlurriness = value;
320
+ }
321
+ break;
322
+ }
323
+ case "background-color": {
324
+ this.applyAttributes();
325
+ break;
326
+ }
327
+ case "public-key": {
328
+ if (newValue != PUBLIC_KEY)
329
+ this.setPublicKey();
330
+ break;
331
+ }
332
+ case "version": {
333
+ if (newValue != VERSION)
334
+ this.setVersion();
335
+ break;
336
+ }
337
+ }
338
+ }
339
+ _loadId = 0;
340
+ _abortController = null;
341
+ _lastSourceFiles = null;
342
+ _createContextPromise = null;
343
+ async onLoad() {
344
+ if (!this.isConnected)
345
+ return;
346
+ if (!this._context) {
347
+ if (debug)
348
+ console.warn("Create new context");
349
+ this._context = new Context({ domElement: this });
350
+ }
351
+ if (!this._context) {
352
+ console.error("Needle Engine: Context not initialized");
353
+ return;
354
+ }
355
+ const filesToLoad = this.getSourceFiles();
356
+ if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
357
+ return;
358
+ }
359
+ // Abort previous loading (if it's still running)
360
+ if (this._abortController) {
361
+ if (debug)
362
+ console.warn("Abort previous loading process");
363
+ this._abortController.abort();
364
+ this._abortController = null;
365
+ }
366
+ this._lastSourceFiles = filesToLoad;
367
+ const loadId = ++this._loadId;
368
+ if (filesToLoad === null || filesToLoad === undefined || filesToLoad.length <= 0) {
369
+ if (debug)
370
+ console.warn("Clear scene", filesToLoad);
371
+ this._context.clear();
372
+ if (loadId !== this._loadId)
373
+ return;
374
+ }
375
+ const alias = this.getAttribute("alias");
376
+ this.classList.add("loading");
377
+ // Loading start events
378
+ const allowOverridingDefaultLoading = hasCommercialLicense();
379
+ // default loading can be overriden by calling preventDefault in the onload start event
380
+ this.ensureLoadStartIsRegistered();
381
+ let useDefaultLoading = this.dispatchEvent(new CustomEvent("loadstart", {
382
+ detail: {
383
+ context: this._context,
384
+ alias: alias
385
+ },
386
+ cancelable: true
387
+ }));
388
+ if (allowOverridingDefaultLoading) {
389
+ // Handle the <needle-engine hide-loading-overlay> attribute
390
+ const hideOverlay = this.getAttribute("hide-loading-overlay");
391
+ if (hideOverlay !== null && hideOverlay !== undefined && hideOverlay !== "0") {
392
+ useDefaultLoading = false;
393
+ }
394
+ }
395
+ // for local development we allow overriding the loading screen - but we notify the user that it won't work in a deployed environment
396
+ if (useDefaultLoading === false && !allowOverridingDefaultLoading) {
397
+ if (!isDevEnvironment())
398
+ useDefaultLoading = true;
399
+ console.warn("Needle Engine: You need a commercial license to override the default loading view. Visit https://needle.tools/pricing");
400
+ if (isDevEnvironment())
401
+ showBalloonWarning("You need a <a target=\"_blank\" href=\"https://needle.tools/pricing\">commercial license</a> to override the default loading view. This will not work in production.");
402
+ }
403
+ // create the loading view idf necessary
404
+ if (!this._loadingView && useDefaultLoading)
405
+ this._loadingView = new EngineLoadingView(this);
406
+ if (useDefaultLoading) {
407
+ // Only show the loading screen immedialty if we haven't loaded anything before
408
+ if (this._didFullyLoad !== true)
409
+ this._loadingView?.onLoadingBegin("begin load");
410
+ else {
411
+ // If we have loaded a glb previously and are loading a new glb due to e.g. src change
412
+ // we don't want to show the loading screen immediately to avoid blinking if the glb to be loaded is tiny
413
+ // so we wait a bit and only show the loading screen if the loading takes longer than a short moment
414
+ setTimeout(() => {
415
+ // If the loading progress is already above ~ 70% we also don't need to show the loading screen anymore
416
+ if (this._loadingView && this._loadingProgress01 < 0.3 && this._loadId === loadId)
417
+ this._loadingView.onLoadingBegin("begin load");
418
+ }, 300);
419
+ }
420
+ }
421
+ if (debug)
422
+ console.warn("--------------\nNeedle Engine: Begin loading " + loadId + "\n", filesToLoad);
423
+ this.onBeforeBeginLoading();
424
+ const loadedFiles = [];
425
+ const progressEventDetail = {
426
+ context: this._context,
427
+ name: "",
428
+ progress: {},
429
+ index: 0,
430
+ count: filesToLoad.length,
431
+ totalProgress01: this._loadingProgress01
432
+ };
433
+ const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
434
+ const displayNames = new Array();
435
+ const controller = new AbortController();
436
+ this._abortController = controller;
437
+ const args = {
438
+ files: filesToLoad,
439
+ abortSignal: controller.signal,
440
+ onLoadingProgress: evt => {
441
+ if (debug)
442
+ console.debug("Loading progress: ", evt);
443
+ if (controller.signal.aborted)
444
+ return;
445
+ const index = evt.index;
446
+ if (!displayNames[index] && evt.name) {
447
+ displayNames[index] = getDisplayName(evt.name);
448
+ }
449
+ evt.name = displayNames[index];
450
+ if (useDefaultLoading)
451
+ this._loadingView?.onLoadingUpdate(evt);
452
+ progressEventDetail.name = evt.name;
453
+ progressEventDetail.progress = evt.progress;
454
+ this._loadingProgress01 = calculateProgress01(evt);
455
+ progressEventDetail.totalProgress01 = this._loadingProgress01;
456
+ this.dispatchEvent(progressEvent);
457
+ },
458
+ onLoadingFinished: (_index, file, glTF) => {
459
+ if (debug)
460
+ console.debug(`Finished loading \"${file}\" (aborted? ${controller.signal.aborted})`);
461
+ if (controller.signal.aborted)
462
+ return;
463
+ if (glTF) {
464
+ loadedFiles.push({
465
+ src: file,
466
+ file: glTF
467
+ });
468
+ }
469
+ }
470
+ };
471
+ const currentHash = this.getAttribute("hash");
472
+ if (currentHash !== null && currentHash !== undefined)
473
+ this._context.hash = currentHash;
474
+ this._context.alias = alias;
475
+ this._createContextPromise = this._context.create(args);
476
+ const success = await this._createContextPromise;
477
+ this.applyAttributes();
478
+ if (debug)
479
+ console.warn("--------------\nNeedle Engine: finished loading " + loadId + "\n", filesToLoad, `Aborted? ${controller.signal.aborted}`);
480
+ if (controller.signal.aborted) {
481
+ console.log("Loading finished but aborted...");
482
+ return;
483
+ }
484
+ if (this._loadId !== loadId) {
485
+ console.log("Load id changed during loading process");
486
+ return;
487
+ }
488
+ this._loadingProgress01 = 1;
489
+ if (useDefaultLoading && success) {
490
+ this._loadingView?.onLoadingUpdate(1, "creating scene");
491
+ }
492
+ this._didFullyLoad = true;
493
+ this.classList.remove("loading");
494
+ this.classList.add("loading-finished");
495
+ this.dispatchEvent(new CustomEvent("loadfinished", {
496
+ detail: {
497
+ context: this._context,
498
+ src: alias,
499
+ loadedFiles: loadedFiles,
500
+ }
501
+ }));
502
+ }
503
+ applyAttributes() {
504
+ // set tonemapping if configured
505
+ if (this._context?.renderer) {
506
+ const attribute = this.getAttribute("tonemapping") || this.getAttribute("tone-mapping");
507
+ switch (attribute?.toLowerCase()) {
508
+ case "none":
509
+ this._context.renderer.toneMapping = NoToneMapping;
510
+ break;
511
+ case "linear":
512
+ this._context.renderer.toneMapping = LinearToneMapping;
513
+ break;
514
+ case "neutral":
515
+ this._context.renderer.toneMapping = NeutralToneMapping;
516
+ break;
517
+ case "agx":
518
+ this._context.renderer.toneMapping = AgXToneMapping;
519
+ break;
520
+ default:
521
+ if (attribute !== null && attribute !== undefined) {
522
+ console.warn("Invalid tone-mapping attribute: " + attribute);
523
+ }
524
+ }
525
+ const exposure = this.getAttribute("tone-mapping-exposure");
526
+ if (exposure !== null && exposure !== undefined) {
527
+ const value = parseFloat(exposure);
528
+ if (!isNaN(value))
529
+ this._context.renderer.toneMappingExposure = value;
530
+ }
531
+ }
532
+ const backgroundBlurriness = this.getAttribute("background-blurriness");
533
+ if (backgroundBlurriness !== null && backgroundBlurriness !== undefined) {
534
+ const value = parseFloat(backgroundBlurriness);
535
+ if (value !== undefined && this._context) {
536
+ this._context.scene.backgroundBlurriness = value;
537
+ }
538
+ }
539
+ const backgroundColor = this.getAttribute("background-color");
540
+ if (this._context?.renderer) {
541
+ if (typeof backgroundColor === "string" && backgroundColor.length > 0) {
542
+ const rgbaColor = RGBAColor.fromColorRepresentation(backgroundColor);
543
+ if (debug)
544
+ console.debug("<needle-engine> background-color changed, str:", backgroundColor, "→", rgbaColor);
545
+ this._context.renderer.setClearColor(rgbaColor, rgbaColor.alpha);
546
+ this.context.scene.background = null;
547
+ }
548
+ }
549
+ }
550
+ onXRSessionStarted = () => {
551
+ const xrSessionMode = this.context.xrSessionMode;
552
+ if (xrSessionMode === "immersive-ar")
553
+ this.onEnterAR(this.context.xrSession);
554
+ else if (xrSessionMode === "immersive-vr")
555
+ this.onEnterVR(this.context.xrSession);
556
+ // handle session end:
557
+ this.context.xrSession?.addEventListener("end", () => {
558
+ this.dispatchEvent(new CustomEvent("xr-session-ended", { detail: { session: this.context.xrSession, context: this._context, sessionMode: xrSessionMode } }));
559
+ if (xrSessionMode === "immersive-ar")
560
+ this.onExitAR(this.context.xrSession);
561
+ else if (xrSessionMode === "immersive-vr")
562
+ this.onExitVR(this.context.xrSession);
563
+ });
564
+ };
565
+ /** called by the context when the first frame has been rendered */
566
+ onReady = () => this._loadingView?.onLoadingFinished();
567
+ onError = () => this._loadingView?.setMessage("Loading failed!");
568
+ internalSetLoadingMessage(str) {
569
+ this._loadingView?.setMessage(str);
570
+ }
571
+ getSourceFiles() {
572
+ const src = this.getAttribute("src");
573
+ if (!src)
574
+ return [];
575
+ let filesToLoad;
576
+ // When using globalThis the src is an array already
577
+ if (Array.isArray(src)) {
578
+ filesToLoad = src;
579
+ }
580
+ // When assigned from codegen the src is a stringified array
581
+ else if (src.startsWith("[") && src.endsWith("]")) {
582
+ filesToLoad = JSON.parse(src);
583
+ }
584
+ // src.toString for an array produces a comma separated list
585
+ else if (src.includes(",")) {
586
+ filesToLoad = src.split(",");
587
+ }
588
+ else
589
+ filesToLoad = [src];
590
+ // filter out invalid or empty strings
591
+ for (let i = filesToLoad.length - 1; i >= 0; i--) {
592
+ const file = filesToLoad[i];
593
+ if (file === "null" || file === "undefined" || file?.length <= 0)
594
+ filesToLoad.splice(i, 1);
595
+ }
596
+ return filesToLoad;
597
+ }
598
+ checkIfSourceHasChanged(current, previous) {
599
+ if (current?.length !== previous?.length)
600
+ return true;
601
+ if (current == null && previous !== null)
602
+ return true;
603
+ if (current !== null && previous == null)
604
+ return true;
605
+ if (current !== null && previous !== null) {
606
+ for (let i = 0; i < current?.length; i++) {
607
+ if (current[i] !== previous[i])
608
+ return true;
609
+ }
610
+ }
611
+ return false;
612
+ }
613
+ _previouslyRegisteredMap = new Map();
614
+ ensureLoadStartIsRegistered() {
615
+ const attributeValue = this.getAttribute("loadstart");
616
+ if (attributeValue)
617
+ this.registerEventFromAttribute("loadstart", attributeValue);
618
+ }
619
+ registerEventFromAttribute(eventName, code) {
620
+ const prev = this._previouslyRegisteredMap.get(eventName);
621
+ if (prev) {
622
+ this._previouslyRegisteredMap.delete(eventName);
623
+ this.removeEventListener(eventName, prev);
624
+ }
625
+ if (typeof code === "string" && code.length > 0) {
626
+ try {
627
+ // indirect eval https://esbuild.github.io/content-types/#direct-eval
628
+ const fn = (0, eval)(code);
629
+ // const fn = new Function(newValue);
630
+ if (typeof fn === "function") {
631
+ this._previouslyRegisteredMap.set(eventName, fn);
632
+ this.addEventListener(eventName, evt => fn?.call(globalThis, this._context, evt));
633
+ }
634
+ }
635
+ catch (err) {
636
+ console.error("Error registering event " + eventName + "=\"" + code + "\" failed with the following error:\n", err);
637
+ }
638
+ }
639
+ }
640
+ setPublicKey() {
641
+ if (PUBLIC_KEY && PUBLIC_KEY.length > 0)
642
+ this.setAttribute("public-key", PUBLIC_KEY);
643
+ }
644
+ setVersion() {
645
+ if (VERSION && VERSION.length > 0) {
646
+ this.setAttribute("version", VERSION);
647
+ }
648
+ }
649
+ /**
650
+ * @internal
651
+ */
652
+ getAROverlayContainer() {
653
+ return this._overlay_ar.createOverlayContainer(this);
654
+ }
655
+ /**
656
+ * @internal
657
+ */
658
+ getVROverlayContainer() {
659
+ for (let i = 0; i < this.children.length; i++) {
660
+ const ch = this.children[i];
661
+ if (ch.classList.contains("vr"))
662
+ return ch;
663
+ }
664
+ return null;
665
+ }
666
+ /**
667
+ * @internal
668
+ */
669
+ onEnterAR(session) {
670
+ this.onSetupAR();
671
+ const overlayContainer = this.getAROverlayContainer();
672
+ this._overlay_ar.onBegin(this._context, overlayContainer, session);
673
+ this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
674
+ }
675
+ /**
676
+ * @internal
677
+ */
678
+ onExitAR(session) {
679
+ this._overlay_ar.onEnd(this._context);
680
+ this.onSetupDesktop();
681
+ this.dispatchEvent(new CustomEvent("exit-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
682
+ }
683
+ /**
684
+ * @internal
685
+ */
686
+ onEnterVR(session) {
687
+ this.onSetupVR();
688
+ this.dispatchEvent(new CustomEvent("enter-vr", { detail: { session: session, context: this._context } }));
689
+ }
690
+ /**
691
+ * @internal
692
+ */
693
+ onExitVR(session) {
694
+ this.onSetupDesktop();
695
+ this.dispatchEvent(new CustomEvent("exit-vr", { detail: { session: session, context: this._context } }));
696
+ }
697
+ onSetupAR() {
698
+ this.classList.add(arSessionActiveClassName);
699
+ this.classList.remove(desktopSessionActiveClassName);
700
+ const arContainer = this.getAROverlayContainer();
701
+ if (debug)
702
+ console.warn("onSetupAR:", arContainer);
703
+ if (arContainer) {
704
+ arContainer.classList.add(arSessionActiveClassName);
705
+ arContainer.classList.remove(desktopSessionActiveClassName);
706
+ }
707
+ this.foreachHtmlElement(ch => this.setupElementsForMode(ch, arContainerClassName));
708
+ }
709
+ onSetupVR() {
710
+ this.classList.remove(arSessionActiveClassName);
711
+ this.classList.remove(desktopSessionActiveClassName);
712
+ this.foreachHtmlElement(ch => this.setupElementsForMode(ch, vrContainerClassName));
713
+ }
714
+ onSetupDesktop() {
715
+ this.classList.remove(arSessionActiveClassName);
716
+ this.classList.add(desktopSessionActiveClassName);
717
+ const arContainer = this.getAROverlayContainer();
718
+ if (arContainer) {
719
+ arContainer.classList.remove(arSessionActiveClassName);
720
+ arContainer.classList.add(desktopSessionActiveClassName);
721
+ }
722
+ this.foreachHtmlElement(ch => this.setupElementsForMode(ch, desktopContainerClassname));
723
+ }
724
+ setupElementsForMode(el, currentSessionType, _session = null) {
725
+ if (el === this._context?.renderer?.domElement)
726
+ return;
727
+ if (el.id === "VRButton" || el.id === "ARButton")
728
+ return;
729
+ const classList = el.classList;
730
+ if (classList.contains(currentSessionType)) {
731
+ el.style.visibility = "visible";
732
+ if (el.style.display === "none")
733
+ el.style.display = "block";
734
+ }
735
+ else {
736
+ // only modify style for elements that have a known class (e.g. marked as vr ar desktop)
737
+ for (const known of knownClasses) {
738
+ if (el.classList.contains(known)) {
739
+ el.style.visibility = "hidden";
740
+ el.style.display = "none";
741
+ }
742
+ }
743
+ }
744
+ }
745
+ foreachHtmlElement(cb) {
746
+ for (let i = 0; i < this.children.length; i++) {
747
+ const ch = this.children[i];
748
+ if (ch.style)
749
+ cb(ch);
750
+ }
751
+ }
752
+ onBeforeBeginLoading() {
753
+ const customDracoDecoderPath = this.getAttribute("dracoDecoderPath");
754
+ if (customDracoDecoderPath) {
755
+ if (debug)
756
+ console.log("using custom draco decoder path", customDracoDecoderPath);
757
+ setDracoDecoderPath(customDracoDecoderPath);
758
+ }
759
+ const customDracoDecoderType = this.getAttribute("dracoDecoderType");
760
+ if (customDracoDecoderType) {
761
+ if (debug)
762
+ console.log("using custom draco decoder type", customDracoDecoderType);
763
+ setDracoDecoderType(customDracoDecoderType);
764
+ }
765
+ const customKtx2DecoderPath = this.getAttribute("ktx2DecoderPath");
766
+ if (customKtx2DecoderPath) {
767
+ if (debug)
768
+ console.log("using custom ktx2 decoder path", customKtx2DecoderPath);
769
+ setKtx2TranscoderPath(customKtx2DecoderPath);
770
+ }
771
+ }
772
+ }
773
+ if (typeof window !== "undefined" && !window.customElements.get(htmlTagName))
774
+ window.customElements.define(htmlTagName, NeedleEngineHTMLElement);
775
+ function getDisplayName(str) {
776
+ if (str.startsWith("blob:")) {
777
+ return "blob";
778
+ }
779
+ const parts = str.split("/");
780
+ let name = parts[parts.length - 1];
781
+ // Remove params
782
+ const paramsIndex = name.indexOf("?");
783
+ if (paramsIndex > 0)
784
+ name = name.substring(0, paramsIndex);
785
+ const equalSign = name.indexOf("=");
786
+ if (equalSign > 0)
787
+ name = name.substring(equalSign);
788
+ const extension = name.split(".").pop();
789
+ const extensions = ["glb", "gltf", "usdz", "usd", "fbx", "obj", "mtl"];
790
+ const matchedIndex = !extension ? -1 : extensions.indexOf(extension.toLowerCase());
791
+ if (extension && matchedIndex >= 0) {
792
+ name = name.substring(0, name.length - extension.length - 1);
793
+ }
794
+ name = decodeURIComponent(name);
795
+ if (name.length > 3) {
796
+ let displayName = "";
797
+ let lastCharacterWasSpace = false;
798
+ const ignoredCharacters = ["(", ")", "[", "]", "{", "}", ":", ";", ",", ".", "!", "?"];
799
+ for (let i = 0; i < name.length; i++) {
800
+ let c = name[i];
801
+ if (c === "_" || c === "-")
802
+ c = " ";
803
+ if (c === ' ' && displayName.length <= 0)
804
+ continue;
805
+ const isIgnored = ignoredCharacters.includes(c);
806
+ if (isIgnored)
807
+ continue;
808
+ const isFirstCharacter = displayName.length === 0;
809
+ if (isFirstCharacter) {
810
+ c = c.toUpperCase();
811
+ }
812
+ if (lastCharacterWasSpace && c === " ") {
813
+ continue;
814
+ }
815
+ if (lastCharacterWasSpace) {
816
+ c = c.toUpperCase();
817
+ }
818
+ lastCharacterWasSpace = false;
819
+ displayName += c;
820
+ if (c === " ") {
821
+ lastCharacterWasSpace = true;
822
+ }
823
+ }
824
+ if (isDevEnvironment() && name !== displayName)
825
+ console.debug("Generated display name: \"" + name + "\" → \"" + displayName + "\"");
826
+ return displayName.trim();
827
+ }
828
+ if (isDevEnvironment())
829
+ console.debug("Loading: use default name", name);
830
+ return name;
831
+ }
832
+ //# sourceMappingURL=needle-engine.js.map