@needle-tools/engine 5.1.0-next.1780579670.dramatic-pierogi.b8fa941 → 5.1.1
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/CHANGELOG.md +119 -0
- package/components.needle.json +1 -1
- package/dist/gltf-progressive-BGQ-cjly.umd.cjs +10 -0
- package/dist/gltf-progressive-BvQHrERj.min.js +10 -0
- package/dist/{gltf-progressive-CH7aqHHr.js → gltf-progressive-CtYnVfLc.js} +420 -351
- package/dist/{materialx-B85WjP7E.min.js → materialx-B5gKeM65.min.js} +1 -1
- package/dist/{materialx-B47Bz-xs.umd.cjs → materialx-CFRcCYWy.umd.cjs} +1 -1
- package/dist/{materialx-jkHmVPez.js → materialx-DqGmteNO.js} +1 -1
- package/dist/{needle-engine.bundle-BSPk6RkD.min.js → needle-engine.bundle-BUT4ua7U.min.js} +162 -150
- package/dist/{needle-engine.bundle-DCPHDPAd.umd.cjs → needle-engine.bundle-Cb39lTJn.umd.cjs} +158 -146
- package/dist/{needle-engine.bundle-WQeWxmZD.js → needle-engine.bundle-Dv-p5m4z.js} +6956 -6782
- package/dist/needle-engine.d.ts +111 -43
- package/dist/needle-engine.js +611 -609
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/three.js +3 -0
- package/dist/three.min.js +22 -22
- package/dist/three.umd.cjs +24 -24
- package/dist/{vendor-BQ2Vuntm.min.js → vendor-2--bD1c9.min.js} +19 -19
- package/dist/{vendor-BslSKZPo.js → vendor-BNtcIZkv.js} +1141 -1138
- package/dist/{vendor-1UvpPPSB.umd.cjs → vendor-vbe9sv1t.umd.cjs} +20 -20
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_context.js +16 -3
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +72 -72
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_networking_peer.js +19 -13
- package/lib/engine/engine_networking_peer.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/postprocessing/postprocessing.d.ts +2 -1
- package/lib/engine/postprocessing/postprocessing.js +3 -2
- package/lib/engine/postprocessing/postprocessing.js.map +1 -1
- package/lib/engine/webcomponents/WebXRButtons.js +5 -6
- package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu.js +9 -9
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +30 -0
- package/lib/engine/xr/NeedleXRSession.js +121 -17
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine/xr/init.js +2 -3
- package/lib/engine/xr/init.js.map +1 -1
- package/lib/engine-components/Component.js +14 -1
- package/lib/engine-components/Component.js.map +1 -1
- package/lib/engine-components/ContactShadows.d.ts +7 -2
- package/lib/engine-components/ContactShadows.js +65 -45
- package/lib/engine-components/ContactShadows.js.map +1 -1
- package/lib/engine-components/GroundProjection.d.ts +6 -0
- package/lib/engine-components/GroundProjection.js +69 -7
- package/lib/engine-components/GroundProjection.js.map +1 -1
- package/lib/engine-components/NeedleMenu.d.ts +1 -1
- package/lib/engine-components/NeedleMenu.js +5 -2
- package/lib/engine-components/NeedleMenu.js.map +1 -1
- package/lib/engine-components/SceneSwitcher.d.ts +2 -0
- package/lib/engine-components/SceneSwitcher.js +26 -14
- package/lib/engine-components/SceneSwitcher.js.map +1 -1
- package/lib/engine-components/SeeThrough.d.ts +39 -0
- package/lib/engine-components/SeeThrough.js +69 -1
- package/lib/engine-components/SeeThrough.js.map +1 -1
- package/lib/engine-components/Skybox.js +9 -0
- package/lib/engine-components/Skybox.js.map +1 -1
- package/lib/engine-components/api.d.ts +1 -0
- package/lib/engine-components/api.js +1 -0
- package/lib/engine-components/api.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +9 -5
- package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
- package/lib/engine-components/particlesystem/ParticleSystem.js +4 -0
- package/lib/engine-components/particlesystem/ParticleSystem.js.map +1 -1
- package/lib/engine-components/postprocessing/Volume.d.ts +1 -9
- package/lib/engine-components/postprocessing/Volume.js +2 -14
- package/lib/engine-components/postprocessing/Volume.js.map +1 -1
- package/lib/engine-components/webxr/WebXR.js +25 -14
- package/lib/engine-components/webxr/WebXR.js.map +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.js +3 -0
- package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
- package/package.json +4 -4
- package/plugins/common/license.js +4 -4
- package/plugins/types/userconfig.d.ts +10 -0
- package/plugins/vite/build-pipeline.d.ts +19 -0
- package/plugins/vite/build-pipeline.js +67 -5
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_context.ts +15 -2
- package/src/engine/engine_gameobject.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +69 -69
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_networking_peer.ts +12 -13
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/postprocessing/postprocessing.ts +3 -2
- package/src/engine/webcomponents/WebXRButtons.ts +5 -6
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +10 -10
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/NeedleXRSession.ts +124 -20
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine/xr/init.ts +2 -3
- package/src/engine-components/Component.ts +13 -1
- package/src/engine-components/ContactShadows.ts +75 -49
- package/src/engine-components/GroundProjection.ts +71 -7
- package/src/engine-components/NeedleMenu.ts +6 -3
- package/src/engine-components/SceneSwitcher.ts +23 -13
- package/src/engine-components/SeeThrough.ts +75 -1
- package/src/engine-components/Skybox.ts +8 -0
- package/src/engine-components/api.ts +1 -0
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +12 -5
- package/src/engine-components/particlesystem/ParticleSystem.ts +4 -0
- package/src/engine-components/postprocessing/Volume.ts +2 -13
- package/src/engine-components/webxr/WebXR.ts +23 -15
- package/src/engine-components/webxr/WebXRImageTracking.ts +1 -0
- package/dist/gltf-progressive-9tlS5iCx.umd.cjs +0 -10
- package/dist/gltf-progressive-CVg80COO.min.js +0 -10
|
@@ -23,6 +23,28 @@ import type { IXRRig } from "./XRRig.js";
|
|
|
23
23
|
const measure_SessionStartedMarker = "NeedleXRSession onStart";
|
|
24
24
|
const measure_SessionEndedMarker = "NeedleXRSession onEnd";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Whether the `xr-spatial-tracking` permission policy is allowed in the current document.
|
|
28
|
+
* WebXR APIs (isSessionSupported / requestSession) require it; an embedded page/iframe must
|
|
29
|
+
* be granted it via the iframe `allow` attribute (delegated from a parent that has it) and/or
|
|
30
|
+
* a `Permissions-Policy` response header. Returns true when the policy API is unavailable
|
|
31
|
+
* (can't determine → don't block, preserve prior behavior).
|
|
32
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#allow
|
|
33
|
+
*/
|
|
34
|
+
function isXrSpatialTrackingAllowed(): boolean {
|
|
35
|
+
try {
|
|
36
|
+
const doc = globalThis.document as (Document & { permissionsPolicy?: { allowsFeature(f: string): boolean }, featurePolicy?: { allowsFeature(f: string): boolean } }) | undefined;
|
|
37
|
+
const policy = doc?.permissionsPolicy ?? doc?.featurePolicy;
|
|
38
|
+
if (policy && typeof policy.allowsFeature === "function") {
|
|
39
|
+
return policy.allowsFeature("xr-spatial-tracking");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* ignore — fall through to default */
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
|
|
27
49
|
/** @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame/fillPoses */
|
|
28
50
|
declare type FillPosesFunction = (spaces: IterableIterator<XRJointSpace>, referenceSpace: XRSpace, targetArray: Float32Array) => void;
|
|
@@ -85,6 +107,10 @@ async function handleSessionGranted() {
|
|
|
85
107
|
let defaultMode: XRSessionMode = "immersive-vr";
|
|
86
108
|
|
|
87
109
|
try {
|
|
110
|
+
// If xr-spatial-tracking isn't allowed in this document (e.g. an embed whose page/iframe
|
|
111
|
+
// wasn't granted it), XR can't run here — skip the probe so we don't log a
|
|
112
|
+
// Permissions-Policy violation on load.
|
|
113
|
+
if (!isXrSpatialTrackingAllowed()) return;
|
|
88
114
|
// In app clips we default to AR
|
|
89
115
|
if (DeviceUtilities.isNeedleAppClip()) {
|
|
90
116
|
defaultMode = "immersive-ar";
|
|
@@ -291,6 +317,51 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
291
317
|
static get currentSessionRequest(): XRSessionMode | null { return this._currentSessionRequestMode; }
|
|
292
318
|
private static _currentSessionRequestMode: XRSessionMode | null = null;
|
|
293
319
|
|
|
320
|
+
/** Base URL for branded Needle AppClip experiences */
|
|
321
|
+
private static readonly appClipBaseUrl = "https://appclip.needle.tools/x/";
|
|
322
|
+
private static _appClipUrl: string | null = null;
|
|
323
|
+
/**
|
|
324
|
+
* Optionally provide a custom Needle AppClip experience to open on iOS instead of the default Needle AppClip.
|
|
325
|
+
* This enables custom App Clip card branding (your own title and image) when launching AR on iPhone and iPad.
|
|
326
|
+
*
|
|
327
|
+
* You can provide either:
|
|
328
|
+
* - the full URL, e.g. `https://appclip.needle.tools/x/your-experience`
|
|
329
|
+
* - or just the experience id, e.g. `your-experience` (expanded to `https://appclip.needle.tools/x/your-experience`)
|
|
330
|
+
*
|
|
331
|
+
* **IMPORTANT**: the resulting URL must exactly match an App Clip experience URL you have registered (with custom card metadata).
|
|
332
|
+
* For this reason the URL is opened as-is, without appending any query parameters.
|
|
333
|
+
*
|
|
334
|
+
* Note: custom App Clip branding requires an active PRO subscription. See the iOS WebXR App Clip docs (Custom Branding for iOS AR).
|
|
335
|
+
* @link https://engine.needle.tools/docs/how-to-guides/xr/ios-webxr-app-clip#custom-branding-for-ios-ar
|
|
336
|
+
*/
|
|
337
|
+
static get appClipUrl(): string | null { return this._appClipUrl; }
|
|
338
|
+
static set appClipUrl(value: string | null) {
|
|
339
|
+
if (!value) {
|
|
340
|
+
this._appClipUrl = null;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const trimmed = value.trim();
|
|
344
|
+
// accept a full URL as-is, otherwise treat the value as an experience id and expand it using the base url
|
|
345
|
+
this._appClipUrl = /^https?:\/\//i.test(trimmed)
|
|
346
|
+
? trimmed
|
|
347
|
+
: this.appClipBaseUrl + trimmed.replace(/^\/+/, "");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @internal
|
|
352
|
+
* Returns the AppClip launch URL to open (or prefetch) on iOS for the current page.
|
|
353
|
+
* When a custom {@link appClipUrl} is set this is the branded experience URL (opened as-is so it matches the registered URL);
|
|
354
|
+
* otherwise it is the default Needle AppClip launch URL with the current page forwarded via the `url` parameter.
|
|
355
|
+
*/
|
|
356
|
+
static getAppClipLaunchUrl(): URL {
|
|
357
|
+
if (this._appClipUrl) return new URL(this._appClipUrl);
|
|
358
|
+
// Forward to the default Needle AppClip experience (using the apple.com url the appclip overlay shows immediately)
|
|
359
|
+
// alternative: `https://appclip.needle.tools/ar?url=${location.href}`
|
|
360
|
+
const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
|
|
361
|
+
url.searchParams.set("url", location.href);
|
|
362
|
+
return url;
|
|
363
|
+
}
|
|
364
|
+
|
|
294
365
|
/**
|
|
295
366
|
* @returns the active @type {NeedleXRSession} (if any active) or null
|
|
296
367
|
*/
|
|
@@ -324,19 +395,49 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
324
395
|
* @param mode The XRSessionMode to check if it is supported
|
|
325
396
|
* @returns true if the browser supports the given XRSessionMode
|
|
326
397
|
*/
|
|
327
|
-
static isSessionSupported(mode: XRSessionMode) {
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
398
|
+
static isSessionSupported(mode: XRSessionMode): Promise<boolean> {
|
|
399
|
+
// Re-probe when XR devices change (e.g. a headset connects).
|
|
400
|
+
this.ensureDeviceChangeInvalidation();
|
|
401
|
+
|
|
402
|
+
// Cache AND de-duplicate the check by storing the in-flight PROMISE (not just the
|
|
403
|
+
// resolved value). A burst of callers in the same tick (XR buttons, offer-session,
|
|
404
|
+
// support checks) then shares ONE underlying navigator.xr.isSessionSupported() call
|
|
405
|
+
// per mode. Each call otherwise repeats the `xr-spatial-tracking` Permissions-Policy
|
|
406
|
+
// check and floods the console. It also keeps user-input onclick checks fast.
|
|
407
|
+
const cached = this._sessionSupportedCache[mode];
|
|
408
|
+
if (cached !== undefined) return cached;
|
|
409
|
+
|
|
410
|
+
let result: Promise<boolean>;
|
|
411
|
+
// isSessionSupported() requires the `xr-spatial-tracking` permission policy to be
|
|
412
|
+
// allowed in this document. In an embed whose page/iframe wasn't granted it, calling
|
|
413
|
+
// the API logs a Permissions-Policy violation and can't succeed — so skip the call
|
|
414
|
+
// and report "unsupported" instead of spamming the console.
|
|
415
|
+
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#allow
|
|
416
|
+
if (!isXrSpatialTrackingAllowed()) {
|
|
417
|
+
if (debug) console.warn("[WebXR] xr-spatial-tracking is not allowed in this document — reporting XR as unsupported. The embedding page/iframe must grant the policy.");
|
|
418
|
+
result = Promise.resolve(false);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
result = (this.xrSystem?.isSessionSupported(mode) ?? Promise.resolve(false))
|
|
422
|
+
.catch(err => { if (debug) console.error(err); return false; });
|
|
423
|
+
}
|
|
424
|
+
this._sessionSupportedCache[mode] = result;
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private static _sessionSupportedCache: { [mode in XRSessionMode]?: Promise<boolean> } = {};
|
|
429
|
+
private static _deviceChangeHooked = false;
|
|
430
|
+
/** Invalidate the support cache when the set of XR devices changes, so a newly
|
|
431
|
+
* connected headset is picked up by the next isSessionSupported() call. */
|
|
432
|
+
private static ensureDeviceChangeInvalidation() {
|
|
433
|
+
if (this._deviceChangeHooked) return;
|
|
434
|
+
this._deviceChangeHooked = true;
|
|
435
|
+
try {
|
|
436
|
+
this.xrSystem?.addEventListener?.("devicechange", () => { this._sessionSupportedCache = {}; });
|
|
437
|
+
}
|
|
438
|
+
catch { /* xrSystem may not support events; cache simply won't be invalidated */ }
|
|
336
439
|
}
|
|
337
440
|
|
|
338
|
-
private static _sessionSupportedCache: { [mode in XRSessionMode]?: boolean } = {};
|
|
339
|
-
|
|
340
441
|
private static _currentSessionRequest?: Promise<XRSession>;
|
|
341
442
|
private static _activeSession: NeedleXRSession | null;
|
|
342
443
|
|
|
@@ -498,10 +599,10 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
498
599
|
const topWindow = window.top || window;
|
|
499
600
|
const originalUrl = topWindow.location.href;
|
|
500
601
|
|
|
501
|
-
// Forward to the AppClip experience
|
|
502
|
-
//
|
|
503
|
-
const
|
|
504
|
-
url.
|
|
602
|
+
// Forward to the AppClip experience. By default this is the Needle AppClip; a custom branded
|
|
603
|
+
// experience can be set via NeedleXRSession.appClipUrl (which is then opened as-is so it matches the registered URL).
|
|
604
|
+
const customAppClip = NeedleXRSession.appClipUrl !== null;
|
|
605
|
+
const url = NeedleXRSession.getAppClipLaunchUrl();
|
|
505
606
|
const urlStr = url.toString();
|
|
506
607
|
|
|
507
608
|
Telemetry.sendEvent(Context.Current, "xr", {
|
|
@@ -513,12 +614,15 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
513
614
|
try {
|
|
514
615
|
console.debug("iOS device detected - opening Needle App Clip for AR experience", { mode, init, url });
|
|
515
616
|
|
|
516
|
-
// Prewarm the appclip by opening the URL
|
|
617
|
+
// Prewarm the appclip by opening the URL (with an extra ?prewarm=1 param) to fetch the app clip
|
|
618
|
+
// metadata faster - this fixes a double-tap issue on iOS.
|
|
517
619
|
// BUT on safari triggering this with multiple URLs causes the
|
|
518
620
|
// Browser to show the dialogue AGAIN in the background
|
|
519
621
|
// SO: we don't do it here...
|
|
520
|
-
|
|
521
|
-
|
|
622
|
+
// We also skip prewarming for a custom branded experience: that URL must match the registered
|
|
623
|
+
// App Clip experience URL exactly, so we must not append any query parameters to it.
|
|
624
|
+
const key = "appclip-prewarm:" + url.origin;
|
|
625
|
+
const allowMultipleNavigations = !customAppClip && sessionStorage.getItem(key) === null;
|
|
522
626
|
if (allowMultipleNavigations) {
|
|
523
627
|
const secondUrl = new URL(url);
|
|
524
628
|
secondUrl.searchParams.set("prewarm", "1");
|
|
@@ -604,7 +708,7 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
604
708
|
// Setup VR initialization parameters
|
|
605
709
|
case "immersive-ar":
|
|
606
710
|
{
|
|
607
|
-
const supported = await
|
|
711
|
+
const supported = await NeedleXRSession.isSessionSupported('immersive-ar')
|
|
608
712
|
if (supported !== true) {
|
|
609
713
|
console.error(mode + ' is not supported by this browser.');
|
|
610
714
|
return null;
|
|
@@ -625,7 +729,7 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
625
729
|
// Setup AR initialization parameters
|
|
626
730
|
case "immersive-vr":
|
|
627
731
|
{
|
|
628
|
-
const supported = await
|
|
732
|
+
const supported = await NeedleXRSession.isSessionSupported('immersive-vr')
|
|
629
733
|
if (supported !== true) {
|
|
630
734
|
console.error(mode + ' is not supported by this browser.');
|
|
631
735
|
return null;
|
|
@@ -3,7 +3,7 @@ import { ArrayCamera, AxesHelper, Camera, Color, DirectionalLight, Fog, GridHelp
|
|
|
3
3
|
import { needleLogoOnlySVG } from "../assets/index.js";
|
|
4
4
|
import { isDevEnvironment } from "../debug/index.js";
|
|
5
5
|
import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js";
|
|
6
|
-
import {
|
|
6
|
+
import { $Mhcqc } from "../engine_license.js";
|
|
7
7
|
import { Mathf } from "../engine_math.js";
|
|
8
8
|
import { delay, DeviceUtilities } from "../engine_utils.js";
|
|
9
9
|
|
|
@@ -215,7 +215,7 @@ export class TemporaryXRContext {
|
|
|
215
215
|
this._scene.background = new Color(0x000000);
|
|
216
216
|
|
|
217
217
|
let logoSrc = needleLogoOnlySVG;
|
|
218
|
-
if (
|
|
218
|
+
if ($Mhcqc()) {
|
|
219
219
|
const htmlComponent = document.querySelector("needle-engine");
|
|
220
220
|
if (htmlComponent) {
|
|
221
221
|
const licenseLogo = htmlComponent.getAttribute("logo-src");
|
package/src/engine/xr/init.ts
CHANGED
|
@@ -12,9 +12,8 @@ export function initXR() {
|
|
|
12
12
|
|
|
13
13
|
if (DeviceUtilities.isiOS()) {
|
|
14
14
|
|
|
15
|
-
// Prefetch
|
|
16
|
-
const url =
|
|
17
|
-
url.searchParams.set("url", location.href);
|
|
15
|
+
// Prefetch (uses the custom branded AppClip experience if NeedleXRSession.appClipUrl is set, otherwise the default Needle AppClip)
|
|
16
|
+
const url = NeedleXRSession.getAppClipLaunchUrl();
|
|
18
17
|
const urlStr = url.toString();
|
|
19
18
|
fetch(urlStr, { method: "HEAD", mode: "no-cors" }).catch(() => {
|
|
20
19
|
// appclip prefetch - to get metadata faster on iOS devices, this seems to fix the double tap issue when opening the appclip for AR sessions.
|
|
@@ -1107,7 +1107,8 @@ export abstract class Component implements IComponent, EventTarget,
|
|
|
1107
1107
|
this.__didCompleteStart = false;
|
|
1108
1108
|
this.__inEnableOrDisableCallback = false;
|
|
1109
1109
|
this.__didEnable = false;
|
|
1110
|
-
|
|
1110
|
+
// Preserve copied/constructor-assigned enabled state while resetting lifecycle flags.
|
|
1111
|
+
// Cloned disabled components must not fall back to the default enabled=true state.
|
|
1111
1112
|
this.__destroyed = false;
|
|
1112
1113
|
this._internalInit(init);
|
|
1113
1114
|
return this;
|
|
@@ -1165,6 +1166,17 @@ export abstract class Component implements IComponent, EventTarget,
|
|
|
1165
1166
|
}
|
|
1166
1167
|
this.__didEnable = true;
|
|
1167
1168
|
this.__isEnabled = true;
|
|
1169
|
+
// Enforce "enabled + not-yet-started ⇒ queued for start". A component can be
|
|
1170
|
+
// re-enabled before it ever started — e.g. removed from the scene (which
|
|
1171
|
+
// strips it from new_script_start via processRemoveFromScene) and then
|
|
1172
|
+
// re-activated through a path other than GameObject.add (raw scene re-add +
|
|
1173
|
+
// setActive, a parent activeInHierarchy flip, …). Those paths re-run onEnable
|
|
1174
|
+
// but never re-enqueue start, leaving the component enabled+active but with
|
|
1175
|
+
// start() never called. Re-queue it here so processStart picks it up.
|
|
1176
|
+
if (!this.__didStart) {
|
|
1177
|
+
const startQueue = this.context?.new_script_start;
|
|
1178
|
+
if (startQueue && !startQueue.includes(this)) startQueue.push(this);
|
|
1179
|
+
}
|
|
1168
1180
|
this.__inEnableOrDisableCallback = true;
|
|
1169
1181
|
this.onEnable();
|
|
1170
1182
|
this.__inEnableOrDisableCallback = false;
|
|
@@ -83,7 +83,7 @@ type FitParameters = {
|
|
|
83
83
|
*/
|
|
84
84
|
export class ContactShadows extends Behaviour {
|
|
85
85
|
|
|
86
|
-
private static readonly _instances:
|
|
86
|
+
private static readonly _instances: WeakMap<Context, ContactShadows> = new WeakMap();
|
|
87
87
|
/**
|
|
88
88
|
* Create contact shadows for the scene. Automatically fits the shadows to the scene.
|
|
89
89
|
* The instance of contact shadows will be created only once.
|
|
@@ -179,8 +179,9 @@ export class ContactShadows extends Behaviour {
|
|
|
179
179
|
* in which case ContactShadows scale would affect the projection
|
|
180
180
|
**/
|
|
181
181
|
private readonly shadowsRoot: IGameObject = new Object3D() as IGameObject;
|
|
182
|
-
private shadowCamera?: OrthographicCamera;
|
|
183
182
|
private readonly shadowGroup: Group = new Group();
|
|
183
|
+
|
|
184
|
+
private shadowCamera?: OrthographicCamera;
|
|
184
185
|
|
|
185
186
|
private renderTarget?: WebGLRenderTarget;
|
|
186
187
|
private renderTargetBlur?: WebGLRenderTarget;
|
|
@@ -240,19 +241,86 @@ export class ContactShadows extends Behaviour {
|
|
|
240
241
|
if (debug) console.log("Fitted shadows to scene", this.shadowsRoot.scale.clone());
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Call to update the shadows immediately. Only needed if `manualUpdate` is enabled, usually the shadows will update automatically when the scene changes or when the component is enabled.
|
|
246
|
+
*/
|
|
247
|
+
renderShadowsNow() {
|
|
248
|
+
this._renderShadows();
|
|
249
|
+
}
|
|
243
250
|
|
|
244
251
|
/** @internal */
|
|
245
252
|
awake() {
|
|
246
253
|
ContactShadows._instances.set(this.context, this);
|
|
247
|
-
|
|
254
|
+
}
|
|
248
255
|
|
|
256
|
+
/** @internal */
|
|
257
|
+
onEnable(): void {
|
|
258
|
+
this.shadowsRoot.hideFlags = HideFlags.DontExport;
|
|
249
259
|
// ignore self for autofitting
|
|
250
260
|
setAutoFitEnabled(this.shadowsRoot, false);
|
|
261
|
+
|
|
262
|
+
this._needsUpdate = true;
|
|
263
|
+
this.autoCleanup(this.context.events.on("scene-content-changed", () => {
|
|
264
|
+
if (!this.autoFit) return;
|
|
265
|
+
this._needsFit = true;
|
|
266
|
+
this._needsUpdate = true;
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
if (this.autoFit) this.fitShadows();
|
|
270
|
+
else this.applyMinSize();
|
|
251
271
|
}
|
|
252
272
|
|
|
273
|
+
/** @internal */
|
|
274
|
+
onDestroy(): void {
|
|
275
|
+
if (debug) console.warn("Destroying ContactShadows on " + this.gameObject.name);
|
|
276
|
+
|
|
277
|
+
const instance = ContactShadows._instances.get(this.context);
|
|
278
|
+
if (instance === this) {
|
|
279
|
+
ContactShadows._instances.delete(this.context);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// dispose the render targets
|
|
283
|
+
this.renderTarget?.dispose();
|
|
284
|
+
this.renderTargetBlur?.dispose();
|
|
285
|
+
|
|
286
|
+
// dispose the materials
|
|
287
|
+
this.depthMaterial?.dispose();
|
|
288
|
+
this.horizontalBlurMaterial?.dispose();
|
|
289
|
+
this.verticalBlurMaterial?.dispose();
|
|
290
|
+
|
|
291
|
+
// dispose the geometries
|
|
292
|
+
this.blurPlane?.geometry.dispose();
|
|
293
|
+
this.plane?.geometry.dispose();
|
|
294
|
+
this.occluderMesh?.geometry.dispose();
|
|
295
|
+
}
|
|
253
296
|
|
|
254
297
|
/** @internal */
|
|
255
|
-
|
|
298
|
+
onBeforeRender(_frame: XRFrame | null): void {
|
|
299
|
+
|
|
300
|
+
if (this._needsFit && this.autoFit) {
|
|
301
|
+
this._needsFit = false;
|
|
302
|
+
this.fitShadows();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (this.manualUpdate) {
|
|
306
|
+
if (!this._needsUpdate) return;
|
|
307
|
+
}
|
|
308
|
+
this._needsUpdate = false;
|
|
309
|
+
this._renderShadows();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private _ensureSetup() {
|
|
313
|
+
|
|
314
|
+
// Make sure the shadows root is parented to the game object. This is needed for the shadows to be in the correct position and scale relative to the game object. We need to do this before checking if the materials are set up because some properties (e.g. darkness) can be changed in the editor before the component is enabled, so we want to avoid re-creating everything in that case.
|
|
315
|
+
if (this.shadowsRoot.parent !== this.gameObject) {
|
|
316
|
+
this.gameObject.add(this.shadowsRoot);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check if everything is already set up. We need to do this because some properties (e.g. darkness) can be changed in the editor before the component is enabled, so we want to avoid re-creating everything in that case.
|
|
320
|
+
if (this.depthMaterial && this.horizontalBlurMaterial && this.verticalBlurMaterial && this.planeMaterial) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
256
324
|
if (debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
|
|
257
325
|
|
|
258
326
|
this.gameObject.add(this.shadowsRoot);
|
|
@@ -363,54 +431,11 @@ export class ContactShadows extends Behaviour {
|
|
|
363
431
|
this.verticalBlurMaterial.depthTest = false;
|
|
364
432
|
|
|
365
433
|
this.shadowGroup.visible = false;
|
|
366
|
-
|
|
367
|
-
if (this.autoFit) this.fitShadows();
|
|
368
|
-
else this.applyMinSize();
|
|
369
434
|
}
|
|
370
435
|
|
|
371
|
-
|
|
372
|
-
this._needsUpdate = true;
|
|
373
|
-
this.autoCleanup(this.context.events.on("scene-content-changed", () => {
|
|
374
|
-
if (!this.autoFit) return;
|
|
375
|
-
this._needsFit = true;
|
|
376
|
-
this._needsUpdate = true;
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
436
|
+
private _renderShadows() {
|
|
379
437
|
|
|
380
|
-
|
|
381
|
-
onDestroy(): void {
|
|
382
|
-
const instance = ContactShadows._instances.get(this.context);
|
|
383
|
-
if (instance === this) {
|
|
384
|
-
ContactShadows._instances.delete(this.context);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// dispose the render targets
|
|
388
|
-
this.renderTarget?.dispose();
|
|
389
|
-
this.renderTargetBlur?.dispose();
|
|
390
|
-
|
|
391
|
-
// dispose the materials
|
|
392
|
-
this.depthMaterial?.dispose();
|
|
393
|
-
this.horizontalBlurMaterial?.dispose();
|
|
394
|
-
this.verticalBlurMaterial?.dispose();
|
|
395
|
-
|
|
396
|
-
// dispose the geometries
|
|
397
|
-
this.blurPlane?.geometry.dispose();
|
|
398
|
-
this.plane?.geometry.dispose();
|
|
399
|
-
this.occluderMesh?.geometry.dispose();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/** @internal */
|
|
403
|
-
onBeforeRender(_frame: XRFrame | null): void {
|
|
404
|
-
|
|
405
|
-
if (this._needsFit && this.autoFit) {
|
|
406
|
-
this._needsFit = false;
|
|
407
|
-
this.fitShadows();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (this.manualUpdate) {
|
|
411
|
-
if (!this._needsUpdate) return;
|
|
412
|
-
}
|
|
413
|
-
this._needsUpdate = false;
|
|
438
|
+
this._ensureSetup();
|
|
414
439
|
|
|
415
440
|
if (!this.renderTarget || !this.renderTargetBlur ||
|
|
416
441
|
!this.depthMaterial || !this.shadowCamera ||
|
|
@@ -536,6 +561,7 @@ export class ContactShadows extends Behaviour {
|
|
|
536
561
|
scene.background = initialBackground;
|
|
537
562
|
renderer.xr.enabled = prevXRState;
|
|
538
563
|
this.context.scene.matrixWorldAutoUpdate = prevSceneMatrixAutoUpdate;
|
|
564
|
+
|
|
539
565
|
}
|
|
540
566
|
|
|
541
567
|
// renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CubeUVReflectionMapping, ShaderMaterial, Texture } from "three";
|
|
1
|
+
import { CubeUVReflectionMapping, Euler, Matrix3, Matrix4, ShaderMaterial, Texture } from "three";
|
|
2
2
|
import { GroundedSkybox as GroundProjection } from 'three/examples/jsm/objects/GroundedSkybox.js';
|
|
3
3
|
|
|
4
4
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
|
@@ -21,8 +21,13 @@ type GroundProjectionShaderUniforms = {
|
|
|
21
21
|
needleGroundProjectionBlending: { value: number };
|
|
22
22
|
needleGroundProjectionAlphaFactor: { value: number };
|
|
23
23
|
needleGroundProjectionBackgroundIntensity: { value: number };
|
|
24
|
+
needleGroundProjectionBackgroundRotation: { value: Matrix3 };
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
// Reused per-frame to build the background-rotation matrix without allocating in onBeforeRender.
|
|
28
|
+
const _backgroundRotationEuler = new Euler();
|
|
29
|
+
const _backgroundRotationMatrix = new Matrix4();
|
|
30
|
+
|
|
26
31
|
const needleCubeUvMapVarying = /* glsl */`
|
|
27
32
|
#ifdef NEEDLE_USE_CUBE_UV_MAP
|
|
28
33
|
varying vec3 vNeedleGroundProjectionWorldDirection;
|
|
@@ -79,7 +84,10 @@ const needleCubeUvMapFragment = /* glsl */`
|
|
|
79
84
|
|
|
80
85
|
#endif
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
// Apply backgroundIntensity uniformly across the whole projection (center included), matching how the
|
|
88
|
+
// skybox applies it (backgroundCube.glsl: texColor.rgb *= backgroundIntensity). Only the blurriness
|
|
89
|
+
// ramps toward the horizon - the intensity must not.
|
|
90
|
+
sampledDiffuseColor.rgb *= needleGroundProjectionBackgroundIntensity;
|
|
83
91
|
diffuseColor *= sampledDiffuseColor;
|
|
84
92
|
|
|
85
93
|
#endif
|
|
@@ -122,7 +130,8 @@ function getGroundProjectionShaderUniforms(material: GroundProjectionMaterial):
|
|
|
122
130
|
needleGroundProjectionBlurriness: { value: 0 },
|
|
123
131
|
needleGroundProjectionBlending: { value: 0 },
|
|
124
132
|
needleGroundProjectionAlphaFactor: { value: 1 },
|
|
125
|
-
needleGroundProjectionBackgroundIntensity: { value: 1 }
|
|
133
|
+
needleGroundProjectionBackgroundIntensity: { value: 1 },
|
|
134
|
+
needleGroundProjectionBackgroundRotation: { value: new Matrix3() }
|
|
126
135
|
};
|
|
127
136
|
}
|
|
128
137
|
|
|
@@ -133,15 +142,25 @@ function configureGroundProjectionMaterial(material: GroundProjectionMaterial, t
|
|
|
133
142
|
shader.uniforms.needleGroundProjectionBlending = projectionUniforms.needleGroundProjectionBlending;
|
|
134
143
|
shader.uniforms.needleGroundProjectionAlphaFactor = projectionUniforms.needleGroundProjectionAlphaFactor;
|
|
135
144
|
shader.uniforms.needleGroundProjectionBackgroundIntensity = projectionUniforms.needleGroundProjectionBackgroundIntensity;
|
|
145
|
+
shader.uniforms.needleGroundProjectionBackgroundRotation = projectionUniforms.needleGroundProjectionBackgroundRotation;
|
|
136
146
|
|
|
137
147
|
shader.vertexShader = shader.vertexShader
|
|
138
|
-
.replace("#include <uv_pars_vertex>", `#include <uv_pars_vertex>\n${needleCubeUvMapVarying}
|
|
148
|
+
.replace("#include <uv_pars_vertex>", `#include <uv_pars_vertex>\n${needleCubeUvMapVarying}
|
|
149
|
+
#ifdef NEEDLE_USE_CUBE_UV_MAP
|
|
150
|
+
uniform mat3 needleGroundProjectionBackgroundRotation;
|
|
151
|
+
#endif`)
|
|
139
152
|
.replace(
|
|
140
153
|
"#include <worldpos_vertex>",
|
|
141
154
|
`#include <worldpos_vertex>
|
|
142
155
|
#ifdef NEEDLE_USE_CUBE_UV_MAP
|
|
143
|
-
//
|
|
144
|
-
|
|
156
|
+
// Sample the environment in the fragment's actual world direction. GroundedSkybox mirrors the geometry
|
|
157
|
+
// on Z (geometry.scale(1,1,-1)) and that mirrored vertex IS the on-screen direction; the skybox and
|
|
158
|
+
// environment lighting are likewise sampled in true world directions. So we must NOT undo the mirror
|
|
159
|
+
// here - doing so reflects the sampling azimuth and flips the panorama left-right vs. the skybox.
|
|
160
|
+
// backgroundRotation is applied to the sampling direction (exactly like the skybox does in
|
|
161
|
+
// backgroundCube.glsl), NOT by rotating the mesh - that keeps the floor flat and matches the skybox at
|
|
162
|
+
// the horizon for any background rotation.
|
|
163
|
+
vNeedleGroundProjectionWorldDirection = needleGroundProjectionBackgroundRotation * transformDirection( position, modelMatrix );
|
|
145
164
|
#endif`
|
|
146
165
|
);
|
|
147
166
|
|
|
@@ -271,6 +290,25 @@ export class GroundProjectedEnv extends Behaviour {
|
|
|
271
290
|
private _projection?: GroundProjection;
|
|
272
291
|
private _watcher?: Watch;
|
|
273
292
|
|
|
293
|
+
/** The scene's real backgroundBlurriness, stored while we temporarily override it for the render. */
|
|
294
|
+
private _savedBackgroundBlur: number | null = null;
|
|
295
|
+
|
|
296
|
+
/** pre_render_callback: force the skybox blur just before the render (after all component onBeforeRender). */
|
|
297
|
+
private readonly _forceBackgroundBlur = (_frame?: XRFrame | null) => {
|
|
298
|
+
if (this._savedBackgroundBlur !== null) return; // already overriding (safety)
|
|
299
|
+
if (!(this.scene.background instanceof Texture)) return; // no skybox to blur
|
|
300
|
+
const current = this.scene.backgroundBlurriness ?? 0;
|
|
301
|
+
this._savedBackgroundBlur = current;
|
|
302
|
+
this.scene.backgroundBlurriness = 1;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** post_render_callback: restore the scene's real blur right after the render. */
|
|
306
|
+
private readonly _restoreBackgroundBlur = () => {
|
|
307
|
+
if (this._savedBackgroundBlur === null) return;
|
|
308
|
+
this.scene.backgroundBlurriness = this._savedBackgroundBlur;
|
|
309
|
+
this._savedBackgroundBlur = null;
|
|
310
|
+
};
|
|
311
|
+
|
|
274
312
|
|
|
275
313
|
/** @internal */
|
|
276
314
|
awake() {
|
|
@@ -291,11 +329,23 @@ export class GroundProjectedEnv extends Behaviour {
|
|
|
291
329
|
this._needsTextureUpdate = true;
|
|
292
330
|
});
|
|
293
331
|
}
|
|
332
|
+
// Force the skybox blur via the render-bracketing callbacks (these run after every component's
|
|
333
|
+
// onBeforeRender, so the override can't be clobbered by e.g. Camera.applyClearFlags).
|
|
334
|
+
if (this.context.pre_render_callbacks.indexOf(this._forceBackgroundBlur) < 0)
|
|
335
|
+
this.context.pre_render_callbacks.push(this._forceBackgroundBlur);
|
|
336
|
+
if (this.context.post_render_callbacks.indexOf(this._restoreBackgroundBlur) < 0)
|
|
337
|
+
this.context.post_render_callbacks.push(this._restoreBackgroundBlur);
|
|
294
338
|
}
|
|
295
339
|
/** @internal */
|
|
296
340
|
onDisable() {
|
|
297
341
|
this._watcher?.revoke();
|
|
298
342
|
this._projection?.removeFromParent();
|
|
343
|
+
const pre = this.context.pre_render_callbacks.indexOf(this._forceBackgroundBlur);
|
|
344
|
+
if (pre >= 0) this.context.pre_render_callbacks.splice(pre, 1);
|
|
345
|
+
const post = this.context.post_render_callbacks.indexOf(this._restoreBackgroundBlur);
|
|
346
|
+
if (post >= 0) this.context.post_render_callbacks.splice(post, 1);
|
|
347
|
+
// If we were disabled mid-frame (after force, before restore), put the real value back.
|
|
348
|
+
this._restoreBackgroundBlur();
|
|
299
349
|
}
|
|
300
350
|
/** @internal */
|
|
301
351
|
onEnterXR(): void {
|
|
@@ -312,7 +362,21 @@ export class GroundProjectedEnv extends Behaviour {
|
|
|
312
362
|
/** @internal */
|
|
313
363
|
onBeforeRender(): void {
|
|
314
364
|
if (this._projection && this.scene.backgroundRotation) {
|
|
315
|
-
|
|
365
|
+
// Mirror three's WebGLBackground: build the same mat3 it applies to the skybox sampling
|
|
366
|
+
// direction and feed it to our shader. This rotates the sampled environment exactly like the
|
|
367
|
+
// skybox (backgroundCube.glsl) instead of rotating the mesh - keeping the floor flat and the
|
|
368
|
+
// horizon aligned with the skybox for any background rotation.
|
|
369
|
+
_backgroundRotationEuler.copy(this.scene.backgroundRotation);
|
|
370
|
+
// accommodate left-handed frame (same as WebGLBackground.js)
|
|
371
|
+
_backgroundRotationEuler.x *= -1;
|
|
372
|
+
_backgroundRotationEuler.y *= -1;
|
|
373
|
+
_backgroundRotationEuler.z *= -1;
|
|
374
|
+
// Note: the extra cube-texture flip in WebGLBackground only applies to raw CubeTextures, not the
|
|
375
|
+
// PMREM render-target env this shader path samples - so it is intentionally omitted here.
|
|
376
|
+
const uniforms = getGroundProjectionShaderUniforms(this._projection.material);
|
|
377
|
+
uniforms.needleGroundProjectionBackgroundRotation.value.setFromMatrix4(
|
|
378
|
+
_backgroundRotationMatrix.makeRotationFromEuler(_backgroundRotationEuler)
|
|
379
|
+
);
|
|
316
380
|
}
|
|
317
381
|
|
|
318
382
|
if (this._projection && this.context.scene.background instanceof Texture) {
|
|
@@ -49,8 +49,8 @@ export class NeedleMenu extends Behaviour {
|
|
|
49
49
|
@serializable()
|
|
50
50
|
position: "top" | "bottom" = "bottom";
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Controls the visibility of the Needle logo in the menu (requires
|
|
52
|
+
/**
|
|
53
|
+
* Controls the visibility of the Needle logo in the menu (requires a commercial license)
|
|
54
54
|
*/
|
|
55
55
|
@serializable()
|
|
56
56
|
showNeedleLogo: boolean = false;
|
|
@@ -102,7 +102,10 @@ export class NeedleMenu extends Behaviour {
|
|
|
102
102
|
this.context.menu.showFullscreenOption(true);
|
|
103
103
|
if (this.createMuteButton === true)
|
|
104
104
|
this.context.menu.showAudioPlaybackOption(true);
|
|
105
|
-
|
|
105
|
+
// Apply the value in BOTH directions: the spatial menu is enabled by
|
|
106
|
+
// default, so unchecking the option must explicitly disable it. Only skip
|
|
107
|
+
// when the field is unset (undefined) to keep the engine default.
|
|
108
|
+
if (this.showSpatialMenu !== undefined)
|
|
106
109
|
this.context.menu.showSpatialMenu(this.showSpatialMenu);
|
|
107
110
|
if (this.createQRCodeButton === true) {
|
|
108
111
|
if (!DeviceUtilities.isMobileDevice()) {
|