@needle-tools/engine 4.12.0-next.fb75c78 → 4.12.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 +11 -5
- package/README.md +4 -2
- package/components.needle.json +1 -1
- package/dist/{gltf-progressive-Rs-ojtXy.umd.cjs → gltf-progressive-Bfpfaz84.umd.cjs} +1 -1
- package/dist/{gltf-progressive-DnLBuGK5.js → gltf-progressive-DPunMlEM.js} +1 -1
- package/dist/{gltf-progressive-BmSygnAC.min.js → gltf-progressive-hFPACYio.min.js} +1 -1
- package/dist/{needle-engine.bundle-CJSpoHVo.min.js → needle-engine.bundle-BPNuWGjI.min.js} +146 -145
- package/dist/{needle-engine.bundle-Cb3SBLtg.umd.cjs → needle-engine.bundle-BV9DUnnS.umd.cjs} +143 -142
- package/dist/{needle-engine.bundle-C5pBHUhB.js → needle-engine.bundle-C4PyE3dQ.js} +7984 -7801
- package/dist/needle-engine.d.ts +15 -8
- package/dist/needle-engine.js +3 -3
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{postprocessing-DZtb9Nnn.umd.cjs → postprocessing-BHQvwehB.umd.cjs} +1 -1
- package/dist/{postprocessing-B5ksn9-G.min.js → postprocessing-ClLv0reO.min.js} +1 -1
- package/dist/{postprocessing-__7s9wON.js → postprocessing-DLI2N3LL.js} +1 -1
- package/dist/{three-examples-y2GeYlze.js → three-examples-D4rE49Ui.js} +10 -2
- package/dist/{three-examples-MsJjauyk.min.js → three-examples-DB5Uoja4.min.js} +2 -2
- package/dist/{three-examples-Dho7cuu4.umd.cjs → three-examples-Djbk6WA4.umd.cjs} +2 -2
- package/lib/engine/engine_context.js +2 -0
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_license.d.ts +18 -0
- package/lib/engine/engine_license.js +161 -9
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking.js +15 -0
- package/lib/engine/engine_networking.js.map +1 -1
- package/lib/engine/engine_serialization_builtin_serializer.js +1 -1
- package/lib/engine/engine_serialization_builtin_serializer.js.map +1 -1
- package/lib/engine/engine_three_utils.js +2 -2
- package/lib/engine/engine_three_utils.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -0
- package/lib/engine/webcomponents/needle menu/needle-menu.js +41 -2
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.js +45 -7
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine-components/DragControls.js +1 -1
- package/lib/engine-components/DragControls.js.map +1 -1
- package/lib/engine-components/DropListener.d.ts +1 -0
- package/lib/engine-components/DropListener.js +26 -8
- package/lib/engine-components/DropListener.js.map +1 -1
- package/lib/engine-components/EventList.js +4 -1
- package/lib/engine-components/EventList.js.map +1 -1
- package/lib/engine-components/SceneSwitcher.d.ts +3 -2
- package/lib/engine-components/SceneSwitcher.js +24 -11
- package/lib/engine-components/SceneSwitcher.js.map +1 -1
- package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +8 -0
- package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
- package/lib/engine-components/webxr/WebARSessionRoot.d.ts +5 -2
- package/lib/engine-components/webxr/WebARSessionRoot.js +5 -2
- package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
- package/lib/engine-components/webxr/WebXR.d.ts +3 -1
- package/lib/engine-components/webxr/WebXR.js +3 -1
- package/lib/engine-components/webxr/WebXR.js.map +1 -1
- package/package.json +3 -3
- package/src/engine/engine_context.ts +2 -0
- package/src/engine/engine_license.ts +178 -9
- package/src/engine/engine_networking.ts +15 -0
- package/src/engine/engine_serialization_builtin_serializer.ts +1 -1
- package/src/engine/engine_three_utils.ts +4 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -1
- package/src/engine/webcomponents/needle menu/needle-menu.ts +44 -3
- package/src/engine/xr/NeedleXRSession.ts +53 -11
- package/src/engine-components/DragControls.ts +1 -1
- package/src/engine-components/DropListener.ts +29 -8
- package/src/engine-components/EventList.ts +5 -1
- package/src/engine-components/SceneSwitcher.ts +26 -13
- package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +11 -0
- package/src/engine-components/webxr/WebARSessionRoot.ts +7 -3
- package/src/engine-components/webxr/WebXR.ts +4 -2
|
@@ -27,6 +27,7 @@ import { ObjectUtils } from "./engine_create_objects.js";
|
|
|
27
27
|
import { destroy, foreachComponent } from './engine_gameobject.js';
|
|
28
28
|
import { getLoader } from './engine_gltf.js';
|
|
29
29
|
import { Input } from './engine_input.js';
|
|
30
|
+
import { Telemetry } from './engine_license.js';
|
|
30
31
|
import { invokeLifecycleFunctions } from './engine_lifecycle_functions_internal.js';
|
|
31
32
|
import { type ILightDataRegistry, LightDataRegistry } from './engine_lightdata.js';
|
|
32
33
|
import { LODsManager } from "./engine_lods.js";
|
|
@@ -1403,6 +1404,7 @@ export class Context implements IContext {
|
|
|
1403
1404
|
if (this._renderlooperrors >= 3) {
|
|
1404
1405
|
console.warn("Stopping render loop due to error")
|
|
1405
1406
|
this.renderer.setAnimationLoop(null);
|
|
1407
|
+
Telemetry.sendError(Context.Current, "renderloop", err instanceof Error ? err : new Error(String(err)) );
|
|
1406
1408
|
}
|
|
1407
1409
|
this.domElement.dispatchEvent(new CustomEvent("error", { detail: err }));
|
|
1408
1410
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { dof } from "three/src/nodes/TSL.js";
|
|
2
|
+
|
|
1
3
|
import { isDevEnvironment } from "./debug/index.js";
|
|
2
4
|
import { BUILD_TIME, GENERATOR, PUBLIC_KEY, VERSION } from "./engine_constants.js";
|
|
3
5
|
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
|
6
|
+
import { onInitialized } from "./engine_lifecycle_api.js";
|
|
7
|
+
import { isLocalNetwork } from "./engine_networking_utils.js";
|
|
4
8
|
import { Context } from "./engine_setup.js";
|
|
5
9
|
import type { IContext } from "./engine_types.js";
|
|
6
10
|
import { getParam } from "./engine_utils.js";
|
|
11
|
+
import { InternalAttributeUtils } from "./engine_utils_attributes.js";
|
|
7
12
|
|
|
8
13
|
const debug = getParam("debuglicense");
|
|
9
14
|
|
|
@@ -67,6 +72,175 @@ function invokeLicenseCheckResultChanged(result: boolean) {
|
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// #region Telemetry
|
|
78
|
+
export namespace Telemetry {
|
|
79
|
+
|
|
80
|
+
window.addEventListener("error", (event: ErrorEvent) => {
|
|
81
|
+
sendError(Context.Current, "unhandled_error", event);
|
|
82
|
+
});
|
|
83
|
+
window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
|
|
84
|
+
sendError(Context.Current, "unhandled_promise_rejection", {
|
|
85
|
+
message: event.reason?.message,
|
|
86
|
+
stack: event.reason?.stack,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
onInitialized((ctx => sendPageViewEvent(ctx)), { once: true });
|
|
92
|
+
|
|
93
|
+
function sendPageViewEvent(ctx: IContext): Promise<void> | void {
|
|
94
|
+
if (!isAllowed(ctx)) {
|
|
95
|
+
if (debug) console.debug("Telemetry is disabled via no-telemetry attribute");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
return doFetch({
|
|
99
|
+
site_id: "dabb8317376f",
|
|
100
|
+
type: "pageview",
|
|
101
|
+
pathname: window.location.pathname,
|
|
102
|
+
hostname: window.location.hostname,
|
|
103
|
+
page_title: document.title,
|
|
104
|
+
referrer: document.referrer,
|
|
105
|
+
user_agent: navigator.userAgent,
|
|
106
|
+
querystring: window.location.search,
|
|
107
|
+
language: navigator.language,
|
|
108
|
+
screenWidth: window.screen.width,
|
|
109
|
+
screenHeight: window.screen.height,
|
|
110
|
+
event_name: "page_view"
|
|
111
|
+
}).then(res => {
|
|
112
|
+
if (res instanceof Response && res.ok && isLocalNetwork()) {
|
|
113
|
+
const src = ctx.domElement?.getAttribute("src") || "";
|
|
114
|
+
const sessionKey = src + VERSION + GENERATOR + BUILD_TIME + PUBLIC_KEY;
|
|
115
|
+
if (window.sessionStorage.getItem("session_key") !== sessionKey) {
|
|
116
|
+
window.sessionStorage.setItem("session_key", sessionKey);
|
|
117
|
+
sendEvent(ctx, "info", {
|
|
118
|
+
src: ctx.domElement?.getAttribute("src") || "",
|
|
119
|
+
version: VERSION,
|
|
120
|
+
generator: GENERATOR,
|
|
121
|
+
build_time: BUILD_TIME,
|
|
122
|
+
public_key: PUBLIC_KEY,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isAllowed(context: IContext | null | undefined): boolean {
|
|
131
|
+
let domElement = context?.domElement as HTMLElement | null;
|
|
132
|
+
if (!domElement) domElement = document.querySelector<HTMLElement>("needle-engine");
|
|
133
|
+
if (!domElement && !context) return false;
|
|
134
|
+
|
|
135
|
+
const attribute = domElement?.getAttribute("no-telemetry");
|
|
136
|
+
if (attribute === "" || attribute === "true" || attribute === "1") {
|
|
137
|
+
if (NEEDLE_ENGINE_LICENSE_TYPE === "pro" || NEEDLE_ENGINE_LICENSE_TYPE === "enterprise") {
|
|
138
|
+
if (debug) console.debug("Telemetry is disabled via no-telemetry attribute");
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const id = "dabb8317376f";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sends a telemetry event
|
|
149
|
+
*/
|
|
150
|
+
export async function sendEvent(context: IContext | null | undefined, eventName: string, properties?: Record<string, any>) {
|
|
151
|
+
if (!isAllowed(context)) {
|
|
152
|
+
if (debug) console.debug("Telemetry is disabled");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const body = {
|
|
156
|
+
site_id: id,
|
|
157
|
+
type: "custom_event",
|
|
158
|
+
pathname: window.location.pathname,
|
|
159
|
+
event_name: eventName,
|
|
160
|
+
properties: properties ? JSON.stringify(properties) : undefined,
|
|
161
|
+
}
|
|
162
|
+
return doFetch(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type ErrorData = {
|
|
166
|
+
message?: string;
|
|
167
|
+
stack?: string;
|
|
168
|
+
filename?: string;
|
|
169
|
+
lineno?: number;
|
|
170
|
+
colno?: number;
|
|
171
|
+
timestamp?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function sendError(context: IContext, errorName: string, error: ErrorData | ErrorEvent | Error) {
|
|
175
|
+
|
|
176
|
+
if (!isAllowed(context)) {
|
|
177
|
+
if (debug) console.debug("Telemetry is disabled");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (error instanceof ErrorEvent) {
|
|
182
|
+
error = {
|
|
183
|
+
message: error.message,
|
|
184
|
+
stack: error.error?.stack,
|
|
185
|
+
filename: error.filename,
|
|
186
|
+
lineno: error.lineno,
|
|
187
|
+
colno: error.colno,
|
|
188
|
+
timestamp: error.timeStamp || Date.now(),
|
|
189
|
+
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
else if (error instanceof Error) {
|
|
193
|
+
error = {
|
|
194
|
+
message: error.message,
|
|
195
|
+
stack: error.stack,
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const body = {
|
|
200
|
+
site_id: id,
|
|
201
|
+
type: "error",
|
|
202
|
+
event_name: errorName || "error",
|
|
203
|
+
properties: JSON.stringify({
|
|
204
|
+
error_name: errorName,
|
|
205
|
+
message: error.message,
|
|
206
|
+
stack: error.stack,
|
|
207
|
+
filename: error.filename,
|
|
208
|
+
lineno: error.lineno,
|
|
209
|
+
colno: error.colno,
|
|
210
|
+
timestamp: error.timestamp,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
return doFetch(body);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function doFetch(body: Record<string, any>) {
|
|
217
|
+
try {
|
|
218
|
+
const url = "https://needle.tools/api/v1/rum/t";
|
|
219
|
+
return fetch(url, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
body: JSON.stringify(body),
|
|
222
|
+
headers: {
|
|
223
|
+
'Content-Type': 'application/json'
|
|
224
|
+
},
|
|
225
|
+
// Ensures request completes even if page unloads
|
|
226
|
+
keepalive: true,
|
|
227
|
+
// Allow CORS requests
|
|
228
|
+
mode: 'cors',
|
|
229
|
+
// Low priority to avoid blocking other requests
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
priority: 'low',
|
|
232
|
+
}).catch(e => {
|
|
233
|
+
if (debug) console.error("Failed to send telemetry", e);
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (debug) console.error(err);
|
|
238
|
+
}
|
|
239
|
+
return Promise.resolve();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
70
244
|
ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
|
|
71
245
|
showLicenseInfo(evt.context);
|
|
72
246
|
handleForbidden(evt.context);
|
|
@@ -341,14 +515,9 @@ async function sendUsageMessageToAnalyticsBackend(context: IContext) {
|
|
|
341
515
|
// We can't send beacons from cross-origin isolated pages
|
|
342
516
|
if (window.crossOriginIsolated) return;
|
|
343
517
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (attribute === "" || attribute === "true" || attribute === "1") {
|
|
348
|
-
if (debug) console.debug("Telemetry is disabled");
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (debug) console.debug("Telemetry attribute: " + attribute);
|
|
518
|
+
if (!Telemetry.isAllowed(context)) {
|
|
519
|
+
if (debug) console.debug("Telemetry is disabled via no-telemetry attribute");
|
|
520
|
+
return;
|
|
352
521
|
}
|
|
353
522
|
|
|
354
523
|
try {
|
|
@@ -379,4 +548,4 @@ async function sendUsageMessageToAnalyticsBackend(context: IContext) {
|
|
|
379
548
|
if (debug)
|
|
380
549
|
console.log("Failed to send non-commercial usage message to analytics backend", err);
|
|
381
550
|
}
|
|
382
|
-
}
|
|
551
|
+
}
|
|
@@ -6,6 +6,7 @@ import { type Websocket } from 'websocket-ts';
|
|
|
6
6
|
|
|
7
7
|
import * as schemes from "../engine-schemes/schemes.js";
|
|
8
8
|
import { isDevEnvironment } from './debug/index.js';
|
|
9
|
+
import { Telemetry } from './engine_license.js';
|
|
9
10
|
import { PeerNetworking } from './engine_networking_peer.js';
|
|
10
11
|
import { type IModel, type INetworkConnection, SendQueue } from './engine_networking_types.js';
|
|
11
12
|
import { isHostedOnGlitch } from './engine_networking_utils.js';
|
|
@@ -658,6 +659,9 @@ export class NetworkConnection implements INetworkConnection {
|
|
|
658
659
|
.onError((_e) => {
|
|
659
660
|
console.error("Websocket connection failed...");
|
|
660
661
|
resolve(false);
|
|
662
|
+
Telemetry.sendEvent(this.context, "networking", {
|
|
663
|
+
event: "connection_error",
|
|
664
|
+
});
|
|
661
665
|
})
|
|
662
666
|
.onRetry(() => { console.log("Retry connecting to networking websocket") })
|
|
663
667
|
.build();
|
|
@@ -727,6 +731,9 @@ export class NetworkConnection implements INetworkConnection {
|
|
|
727
731
|
"server did not send connection id", connection.id);
|
|
728
732
|
console.debug("Your id is: " + connection.id, this.context.alias ?? "");
|
|
729
733
|
this._connectionId = connection.id;
|
|
734
|
+
Telemetry.sendEvent(this.context, "networking", {
|
|
735
|
+
event: "connected",
|
|
736
|
+
});
|
|
730
737
|
}
|
|
731
738
|
}
|
|
732
739
|
else console.warn("Expected connection id in " + message.key);
|
|
@@ -754,6 +761,10 @@ export class NetworkConnection implements INetworkConnection {
|
|
|
754
761
|
}
|
|
755
762
|
|
|
756
763
|
this.onSendQueued(SendQueue.OnRoomJoin);
|
|
764
|
+
Telemetry.sendEvent(this.context, "networking", {
|
|
765
|
+
event: "joined_room",
|
|
766
|
+
room: this._currentRoomName,
|
|
767
|
+
});
|
|
757
768
|
break;
|
|
758
769
|
|
|
759
770
|
case RoomEvents.LeftRoom:
|
|
@@ -765,6 +776,10 @@ export class NetworkConnection implements INetworkConnection {
|
|
|
765
776
|
this._currentInRoom.length = 0;
|
|
766
777
|
if (debugnetBin || isDevEnvironment()) console.debug("Left Needle Engine Room: " + model.room);
|
|
767
778
|
}
|
|
779
|
+
Telemetry.sendEvent(this.context, "networking", {
|
|
780
|
+
event: "left_room",
|
|
781
|
+
room: model.room,
|
|
782
|
+
});
|
|
768
783
|
break;
|
|
769
784
|
case RoomEvents.UserJoinedRoom:
|
|
770
785
|
if (message.data) {
|
|
@@ -337,7 +337,7 @@ class EventListSerializer extends TypeSerializer {
|
|
|
337
337
|
args = call.arguments.map(deserializeArgument);
|
|
338
338
|
}
|
|
339
339
|
const method = target[call.method];
|
|
340
|
-
if (
|
|
340
|
+
if (method === undefined) {
|
|
341
341
|
console.warn(`EventList method not found: \"${call.method}\" on ${target?.name}`);
|
|
342
342
|
}
|
|
343
343
|
else {
|
|
@@ -242,8 +242,10 @@ export function setWorldPosition(obj: Object3D, val: Vector3): Object3D {
|
|
|
242
242
|
const wp = _worldPositions.get();
|
|
243
243
|
if (val !== wp)
|
|
244
244
|
wp.copy(val);
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
|
|
246
|
+
if (obj.parent !== null)
|
|
247
|
+
obj.parent.worldToLocal(wp);
|
|
248
|
+
|
|
247
249
|
obj.position.set(wp.x, wp.y, wp.z);
|
|
248
250
|
return obj;
|
|
249
251
|
}
|
|
@@ -104,7 +104,8 @@ export class NeedleSpatialMenu {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const xr = this._context.xr;
|
|
107
|
-
|
|
107
|
+
const isImmersiveXR = xr?.running && (xr?.isPassThrough || xr?.isVR)
|
|
108
|
+
if (!isImmersiveXR) {
|
|
108
109
|
if (this._wasInXR) {
|
|
109
110
|
this._wasInXR = false;
|
|
110
111
|
this.onExitXR();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { showBalloonMessage } from "../../debug/debug.js";
|
|
1
2
|
import type { Context } from "../../engine_context.js";
|
|
2
|
-
import { hasCommercialLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
|
|
3
|
+
import { hasCommercialLicense, onLicenseCheckResultChanged, Telemetry } from "../../engine_license.js";
|
|
3
4
|
import { isLocalNetwork } from "../../engine_networking_utils.js";
|
|
4
5
|
import { DeviceUtilities, getParam } from "../../engine_utils.js";
|
|
5
6
|
import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
|
|
@@ -149,6 +150,9 @@ export class NeedleMenu {
|
|
|
149
150
|
else console.error("NeedleMenu: onclick is not a valid link", buttoninfo.onclick);
|
|
150
151
|
}
|
|
151
152
|
}
|
|
153
|
+
Telemetry.sendEvent(this._context, "needle-menu", {
|
|
154
|
+
action: "button_added_via_postmessage",
|
|
155
|
+
});
|
|
152
156
|
this._menu.appendChild(button);
|
|
153
157
|
}
|
|
154
158
|
else if (debug) console.error("NeedleMenu: unknown postMessage event", data);
|
|
@@ -935,12 +939,39 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
935
939
|
/** @private foldout container used in compact mode */
|
|
936
940
|
private readonly foldout: HTMLDivElement;
|
|
937
941
|
|
|
942
|
+
|
|
943
|
+
private readonly trackedElements: WeakSet<Node> = new WeakSet();
|
|
944
|
+
private trackElement(el: Node) {
|
|
945
|
+
if (this.trackedElements.has(el)) return;
|
|
946
|
+
this.trackedElements.add(el);
|
|
947
|
+
el.addEventListener("click", (evt) => {
|
|
948
|
+
Telemetry.sendEvent(this._context, "needle-menu", {
|
|
949
|
+
action: "button_clicked",
|
|
950
|
+
element: evt.target instanceof Node ? evt.target.nodeName : el.nodeName,
|
|
951
|
+
label: el.textContent,
|
|
952
|
+
title: (el instanceof HTMLElement) ? el.title : undefined,
|
|
953
|
+
pointerid: (evt instanceof PointerEvent) ? evt.pointerId : undefined,
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
// el.addEventListener("pointerenter", (evt) => {
|
|
957
|
+
// Telemetry.sendEvent(this._context, "needle-menu", {
|
|
958
|
+
// action: "button_hovered",
|
|
959
|
+
// element: evt.target instanceof Node ? evt.target.nodeName : el.nodeName,
|
|
960
|
+
// label: el.textContent,
|
|
961
|
+
// title: (el instanceof HTMLElement) ? el.title : undefined,
|
|
962
|
+
// pointerid: (evt instanceof PointerEvent) ? evt.pointerId : undefined,
|
|
963
|
+
// });
|
|
964
|
+
// });
|
|
965
|
+
}
|
|
966
|
+
|
|
938
967
|
append(...nodes: (string | Node)[]): void {
|
|
939
968
|
for (const node of nodes) {
|
|
940
969
|
if (typeof node === "string") {
|
|
941
970
|
const element = document.createTextNode(node);
|
|
971
|
+
this.trackElement(element);
|
|
942
972
|
this.options.appendChild(element);
|
|
943
973
|
} else {
|
|
974
|
+
this.trackElement(node);
|
|
944
975
|
this.options.appendChild(node);
|
|
945
976
|
}
|
|
946
977
|
}
|
|
@@ -987,9 +1018,10 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
987
1018
|
if (node.class) {
|
|
988
1019
|
button.classList.add(node.class);
|
|
989
1020
|
}
|
|
1021
|
+
|
|
990
1022
|
node = button as unknown as T;
|
|
991
1023
|
}
|
|
992
|
-
|
|
1024
|
+
this.trackElement(node);
|
|
993
1025
|
const res = this.options.appendChild(node);
|
|
994
1026
|
return res;
|
|
995
1027
|
}
|
|
@@ -997,8 +1029,10 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
997
1029
|
for (const node of nodes) {
|
|
998
1030
|
if (typeof node === "string") {
|
|
999
1031
|
const element = document.createTextNode(node);
|
|
1032
|
+
this.trackElement(element);
|
|
1000
1033
|
this.options.prepend(element);
|
|
1001
1034
|
} else {
|
|
1035
|
+
this.trackElement(node);
|
|
1002
1036
|
this.options.prepend(node);
|
|
1003
1037
|
}
|
|
1004
1038
|
}
|
|
@@ -1142,8 +1176,15 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
1142
1176
|
const getCurrentWidth = () => {
|
|
1143
1177
|
return this.options.clientWidth + this.logoContainer.clientWidth;
|
|
1144
1178
|
}
|
|
1179
|
+
|
|
1180
|
+
let lastSpaceLeft = -1;
|
|
1145
1181
|
const getSpaceLeft = () => {
|
|
1146
|
-
|
|
1182
|
+
const spaceLeft = availableWidth - getCurrentWidth();
|
|
1183
|
+
if (debug && spaceLeft !== lastSpaceLeft) {
|
|
1184
|
+
lastSpaceLeft = spaceLeft;
|
|
1185
|
+
showBalloonMessage(`Menu space left: ${spaceLeft.toFixed(0)}px`);
|
|
1186
|
+
}
|
|
1187
|
+
return spaceLeft;
|
|
1147
1188
|
}
|
|
1148
1189
|
}
|
|
1149
1190
|
|
|
@@ -6,6 +6,7 @@ import { Context, FrameEvent } from "../engine_context.js";
|
|
|
6
6
|
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
|
7
7
|
import { isDestroyed } from "../engine_gameobject.js";
|
|
8
8
|
import { Gizmos } from "../engine_gizmos.js";
|
|
9
|
+
import { Telemetry } from "../engine_license.js";
|
|
9
10
|
import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
|
|
10
11
|
import { getBoundingBox, getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
|
|
11
12
|
import type { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
|
@@ -98,12 +99,12 @@ async function handleSessionGranted() {
|
|
|
98
99
|
}
|
|
99
100
|
// Check if AR is supported, otherwise we can't do anything
|
|
100
101
|
if (!(await navigator.xr?.isSessionSupported("immersive-ar")) && defaultMode === "immersive-ar") {
|
|
101
|
-
console.warn("[NeedleXRSession:granted] Neither VR nor AR supported, aborting session start.");
|
|
102
|
+
// console.warn("[NeedleXRSession:granted] Neither VR nor AR supported, aborting session start.");
|
|
102
103
|
// showBalloonMessage("NeidleXRSession: Neither VR nor AR supported, aborting session start.");
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
106
|
} catch (e) {
|
|
106
|
-
console.
|
|
107
|
+
console.debug("[NeedleXRSession:granted] Error while checking XR support:", e);
|
|
107
108
|
// showBalloonWarning("NeedleXRSession: Error while checking XR support: " + (e as Error).message);
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
@@ -148,6 +149,7 @@ async function handleSessionGranted() {
|
|
|
148
149
|
navigator.xr?.addEventListener('sessiongranted', async () => {
|
|
149
150
|
// enableSpatialConsole(true);
|
|
150
151
|
|
|
152
|
+
|
|
151
153
|
const lastSessionMode = sessionStorage.getItem("needle_xr_session_mode") as XRSessionMode;
|
|
152
154
|
const lastSessionInit = sessionStorage.getItem("needle_xr_session_init") ?? null;
|
|
153
155
|
const init = lastSessionInit ? JSON.parse(lastSessionInit) : null;
|
|
@@ -459,27 +461,55 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
459
461
|
if (DeviceUtilities.isiOS()) {
|
|
460
462
|
|
|
461
463
|
const arSupported = await this.isARSupported().catch(() => false);
|
|
462
|
-
|
|
464
|
+
|
|
463
465
|
// On VisionOS, we use QuickLook for AR experiences; no AppClip support for now.
|
|
464
466
|
if (DeviceUtilities.isVisionOS() && !arSupported && (mode === "ar" || mode === "immersive-ar")) {
|
|
465
467
|
mode = "quicklook";
|
|
466
468
|
}
|
|
467
469
|
|
|
468
470
|
if (mode === "quicklook") {
|
|
471
|
+
Telemetry.sendEvent(Context.Current, "xr", {
|
|
472
|
+
action: "quicklook_export",
|
|
473
|
+
source: "NeedleXRSession.start",
|
|
474
|
+
});
|
|
469
475
|
InternalUSDZRegistry.exportAndOpen();
|
|
470
476
|
return null;
|
|
471
477
|
}
|
|
472
|
-
|
|
478
|
+
|
|
473
479
|
if (!arSupported && (mode === "immersive-ar" || mode === "ar")) {
|
|
474
480
|
// const debugAppClip = getParam("debugappclip")
|
|
475
481
|
// Forward to the AppClip experience (Using the apple.com url the appclip overlay shows immediately)
|
|
476
|
-
const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
|
|
477
|
-
url.searchParams.set("url", location.href);
|
|
478
482
|
// const url =`https://appclip.needle.tools/ar?url=${(location.href)}`;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
+
const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
|
|
484
|
+
url.searchParams.set("url", location.href);
|
|
485
|
+
|
|
486
|
+
const urlStr = url.toString();
|
|
487
|
+
|
|
488
|
+
Telemetry.sendEvent(Context.Current, "xr", {
|
|
489
|
+
action: "app_clip_launch",
|
|
490
|
+
source: "NeedleXRSession.start",
|
|
491
|
+
url: urlStr,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// if we are in an iframe, we need to navigate the top window
|
|
495
|
+
const topWindow = window.top || window;
|
|
496
|
+
try {
|
|
497
|
+
console.debug("iOS device detected - opening Needle App Clip for AR experience", { mode, init, url });
|
|
498
|
+
// navigate to app clip url but keep the current url in history, open in same tab
|
|
499
|
+
// eslint-disable-next-line xss/no-location-href-assign
|
|
500
|
+
topWindow.location.href = urlStr;
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
console.warn("Error navigating to AppClip " + urlStr + "\n", e);
|
|
504
|
+
// if top window navigation fails and we are in an iframe, we try to navigate the top window directly
|
|
505
|
+
const weAreInIframe = window !== window.top;
|
|
506
|
+
if (weAreInIframe) {
|
|
507
|
+
// we can try to open a new tab as a fallback
|
|
508
|
+
window.open(urlStr, "_blank");
|
|
509
|
+
}
|
|
510
|
+
// eslint-disable-next-line xss/no-location-href-assign
|
|
511
|
+
else window.location.href = urlStr;
|
|
512
|
+
}
|
|
483
513
|
|
|
484
514
|
return null;
|
|
485
515
|
}
|
|
@@ -489,7 +519,7 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
489
519
|
console.warn("QuickLook mode is only supported on iOS devices");
|
|
490
520
|
return null;
|
|
491
521
|
}
|
|
492
|
-
|
|
522
|
+
|
|
493
523
|
// Since we now know we are not on iOS, ar mode becomes "immersive-ar"
|
|
494
524
|
if (mode == "ar") {
|
|
495
525
|
mode = "immersive-ar";
|
|
@@ -589,6 +619,12 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
589
619
|
listener({ mode, init });
|
|
590
620
|
}
|
|
591
621
|
if (debug) showBalloonMessage("Requesting " + mode + " session (" + Date.now() + ")");
|
|
622
|
+
Telemetry.sendEvent(Context.Current, "xr", {
|
|
623
|
+
action: "session_request",
|
|
624
|
+
mode: mode,
|
|
625
|
+
features: ((init.requiredFeatures ?? []).concat(init.optionalFeatures ?? [])).join(","),
|
|
626
|
+
source: "NeedleXRSession.start",
|
|
627
|
+
});
|
|
592
628
|
this._currentSessionRequest = navigator?.xr?.requestSession(mode, init);
|
|
593
629
|
this._currentSessionRequestMode = mode;
|
|
594
630
|
/**@type {XRSystem} */
|
|
@@ -1165,6 +1201,12 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
1165
1201
|
|
|
1166
1202
|
console.debug("XR Session ended");
|
|
1167
1203
|
|
|
1204
|
+
Telemetry.sendEvent(Context.Current, "xr", {
|
|
1205
|
+
action: "session_end",
|
|
1206
|
+
mode: this.mode,
|
|
1207
|
+
source: "NeedleXRSession.onEnd",
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1168
1210
|
deleteSessionInfo();
|
|
1169
1211
|
|
|
1170
1212
|
this.onAfterRender();
|
|
@@ -447,7 +447,7 @@ export class DragControls extends Behaviour implements IPointerEventHandler {
|
|
|
447
447
|
if (!this || !this._isDragging) return;
|
|
448
448
|
this._isDragging = false;
|
|
449
449
|
for (const rb of this._draggingRigidbodies) {
|
|
450
|
-
rb.setVelocity(rb.smoothedVelocity);
|
|
450
|
+
rb.setVelocity(rb.smoothedVelocity.multiplyScalar(this.context.time.deltaTime));
|
|
451
451
|
}
|
|
452
452
|
this._draggingRigidbodies.length = 0;
|
|
453
453
|
this._targetObject = null;
|
|
@@ -10,7 +10,7 @@ import { BlobStorage } from "../engine/engine_networking_blob.js";
|
|
|
10
10
|
import { PreviewHelper } from "../engine/engine_networking_files.js";
|
|
11
11
|
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
|
12
12
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
|
13
|
-
import { fitObjectIntoVolume, getBoundingBox, placeOnSurface } from "../engine/engine_three_utils.js";
|
|
13
|
+
import { fitObjectIntoVolume, getBoundingBox, getWorldScale, placeOnSurface } from "../engine/engine_three_utils.js";
|
|
14
14
|
import { Model, Vec3 } from "../engine/engine_types.js";
|
|
15
15
|
import { getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
|
16
16
|
import { determineMimeTypeFromExtension } from "../engine/engine_utils_format.js";
|
|
@@ -111,6 +111,7 @@ const blobKeyName = "blob";
|
|
|
111
111
|
|
|
112
112
|
/** The DropListener component is used to listen for drag and drop events in the browser and add the dropped files to the scene
|
|
113
113
|
* It can be used to allow users to drag and drop glTF files into the scene to add new objects.
|
|
114
|
+
* Existing child objects will behave like placeholders and will be removed when new files are dropped.
|
|
114
115
|
*
|
|
115
116
|
* If {@link useNetworking} is enabled, the DropListener will automatically synchronize dropped files to other connected clients.
|
|
116
117
|
* Enable {@link fitIntoVolume} to automatically scale dropped objects to fit within the volume defined by {@link fitVolumeSize}.
|
|
@@ -522,22 +523,42 @@ export class DropListener extends Behaviour {
|
|
|
522
523
|
|
|
523
524
|
const obj = model.scene;
|
|
524
525
|
|
|
525
|
-
|
|
526
|
-
this.gameObject
|
|
527
|
-
obj.position.set(0, 0, 0);
|
|
528
|
-
obj.quaternion.identity();
|
|
526
|
+
obj.position.copy(this.gameObject.worldPosition);
|
|
527
|
+
const scale = getWorldScale(this.gameObject);
|
|
529
528
|
|
|
530
|
-
|
|
531
|
-
|
|
529
|
+
let localPos = new Vector3(0,0,0);
|
|
530
|
+
scale.x = Math.abs(scale.x);
|
|
531
|
+
scale.y = Math.abs(scale.y);
|
|
532
|
+
scale.z = Math.abs(scale.z);
|
|
533
|
+
let localScale =obj.scale.clone();
|
|
534
|
+
|
|
535
|
+
// TODOs: handle rotation when Gizmos APIs has changed to support it
|
|
532
536
|
|
|
533
|
-
const volume = new Box3().setFromCenterAndSize(new Vector3(0, this.fitVolumeSize.y * .5, 0).add(this.gameObject.worldPosition), this.fitVolumeSize);
|
|
537
|
+
const volume = new Box3().setFromCenterAndSize(new Vector3(0, this.fitVolumeSize.y * scale.y * .5, 0).add(this.gameObject.worldPosition), this.fitVolumeSize.clone().multiply(scale));
|
|
534
538
|
if (debug) Gizmos.DrawWireBox3(volume, 0x0000ff, 5);
|
|
535
539
|
if (this.fitIntoVolume) {
|
|
540
|
+
|
|
536
541
|
fitObjectIntoVolume(obj, volume, {
|
|
537
542
|
position: !this.placeAtHitPosition
|
|
538
543
|
});
|
|
544
|
+
|
|
545
|
+
// to match parent scale later, divide by it
|
|
546
|
+
localScale = obj.scale.clone().divide(scale);
|
|
547
|
+
// just take the computed offset from fitting
|
|
548
|
+
localPos = obj.worldPosition.clone().sub(this.gameObject.worldPosition).divide(scale);
|
|
549
|
+
if (debug) Gizmos.DrawSphere(localPos, 0.1, 0xff0000, 5);
|
|
539
550
|
}
|
|
540
551
|
|
|
552
|
+
// use attach to ignore the DropListener scale (e.g. if the parent object scale is not uniform)
|
|
553
|
+
this.gameObject.attach(obj);
|
|
554
|
+
obj.position.copy(localPos);
|
|
555
|
+
obj.quaternion.identity();
|
|
556
|
+
obj.scale.copy(localScale);
|
|
557
|
+
if (debug) Gizmos.DrawArrow(this.gameObject.worldPosition, obj.getWorldPosition(new Vector3()), 0x00ff00, 5);
|
|
558
|
+
|
|
559
|
+
this._addedObjects.push(obj);
|
|
560
|
+
this._addedModels.push(model);
|
|
561
|
+
|
|
541
562
|
if (this.placeAtHitPosition && ctx && ctx.screenposition) {
|
|
542
563
|
obj.visible = false; // < don't raycast on the placed object
|
|
543
564
|
const rc = this.context.physics.raycast({ screenPoint: this.context.input.convertScreenspaceToRaycastSpace(ctx.screenposition.clone()) });
|
|
@@ -78,7 +78,11 @@ export class CallInfo {
|
|
|
78
78
|
// If the target is a property
|
|
79
79
|
else {
|
|
80
80
|
if (this.arguments) {
|
|
81
|
-
|
|
81
|
+
|
|
82
|
+
if (args !== undefined && args.length > 0)
|
|
83
|
+
this.target[this.methodName] = args[0];
|
|
84
|
+
else
|
|
85
|
+
this.target[this.methodName] = this.arguments[0];
|
|
82
86
|
}
|
|
83
87
|
else {
|
|
84
88
|
this.target[this.methodName] = args[0];
|