@needle-tools/engine 5.1.1 → 5.1.2
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 +6 -0
- package/dist/{needle-engine.bundle-Dv-p5m4z.js → needle-engine.bundle-BAUfRyBA.js} +2861 -2708
- package/dist/{needle-engine.bundle-Cb39lTJn.umd.cjs → needle-engine.bundle-BL2wG5Te.umd.cjs} +101 -101
- package/dist/{needle-engine.bundle-BUT4ua7U.min.js → needle-engine.bundle-BpNRkY3s.min.js} +148 -148
- package/dist/needle-engine.d.ts +18 -6
- package/dist/needle-engine.js +401 -401
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- 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_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +45 -7
- package/lib/engine/engine_license.js +276 -77
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking.js +6 -0
- package/lib/engine/engine_networking.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_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.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.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +31 -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/NeedleXRSync.js +23 -4
- package/lib/engine/xr/NeedleXRSync.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/SyncedTransform.d.ts +5 -1
- package/lib/engine-components/SyncedTransform.js +45 -8
- package/lib/engine-components/SyncedTransform.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/webxr/Avatar.d.ts +8 -0
- package/lib/engine-components/webxr/Avatar.js +60 -32
- package/lib/engine-components/webxr/Avatar.js.map +1 -1
- package/lib/engine-schemes/synced-transform-model.d.ts +2 -0
- package/lib/engine-schemes/synced-transform-model.js +8 -1
- package/lib/engine-schemes/synced-transform-model.js.map +1 -1
- package/package.json +1 -1
- package/plugins/common/license.js +4 -4
- package/plugins/types/userconfig.d.ts +4 -3
- package/plugins/vite/build-pipeline.d.ts +3 -1
- package/plugins/vite/build-pipeline.js +19 -9
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +263 -73
- package/src/engine/engine_networking.ts +5 -0
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +31 -2
- package/src/engine/xr/NeedleXRSync.ts +24 -4
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/SyncedTransform.ts +49 -10
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/webxr/Avatar.ts +60 -33
- package/src/engine-schemes/synced-transform-model.ts +10 -1
- package/src/engine-schemes/transforms.fbs +1 -0
|
@@ -4,7 +4,7 @@ import { Quaternion, Vector2, Vector3, Vector4 } from "three";
|
|
|
4
4
|
|
|
5
5
|
import { needleLogoOnlySVG } from "./assets/index.js";
|
|
6
6
|
import { isDevEnvironment } from "./debug/debug.js";
|
|
7
|
-
import {
|
|
7
|
+
import { Jkdun } from "./engine_license.js";
|
|
8
8
|
import { InternalAttributeUtils } from "./engine_utils_attributes.js";
|
|
9
9
|
import type { NeedleEngineWebComponent } from "./webcomponents/needle-engine.js";
|
|
10
10
|
|
|
@@ -135,7 +135,7 @@ async function internalRenderQRCodeOverlays(canvas: HTMLCanvasElement, args: { s
|
|
|
135
135
|
console.debug("[QR Code] No web component found")
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const canUseCustomLogo =
|
|
138
|
+
const canUseCustomLogo = Jkdun();
|
|
139
139
|
|
|
140
140
|
// Query logo src from needle-engine attribute.
|
|
141
141
|
// For any supported attribute it's possible to use "falsey" values (e.g. "0" or "false" to disable the logo in the QR code)
|
|
@@ -2,7 +2,7 @@ import { Mesh, Object3D, TextureLoader, Vector4 } from "three";
|
|
|
2
2
|
import ThreeMeshUI from "three-mesh-ui";
|
|
3
3
|
|
|
4
4
|
import { addNewComponent } from "../../engine_components.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Jkdun } from "../../engine_license.js";
|
|
6
6
|
import { OneEuroFilterXYZ } from "../../engine_math.js";
|
|
7
7
|
import type { Context } from "../../engine_setup.js";
|
|
8
8
|
import { lookAtObject } from "../../engine_three_utils.js";
|
|
@@ -321,7 +321,7 @@ export class NeedleSpatialMenu {
|
|
|
321
321
|
}
|
|
322
322
|
if (this.menu) {
|
|
323
323
|
const index = this.menu.children.indexOf(this._poweredByNeedleElement as any);
|
|
324
|
-
if (!this._showNeedleLogo &&
|
|
324
|
+
if (!this._showNeedleLogo && Jkdun()) {
|
|
325
325
|
if (index >= 0) {
|
|
326
326
|
this._poweredByNeedleElement.removeFromParent();
|
|
327
327
|
this.markDirty();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { showBalloonMessage } from "../../debug/debug.js";
|
|
2
2
|
import type { Context } from "../../engine_context.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Jkdun, qWc, Telemetry } from "../../engine_license.js";
|
|
4
4
|
import { isLocalNetwork } from "../../engine_networking_utils.js";
|
|
5
5
|
import { HTMLElementBase } from "../../engine_ssr.js";
|
|
6
6
|
import { DeviceUtilities, getParam } from "../../engine_utils.js";
|
|
@@ -778,8 +778,8 @@ export class NeedleMenuElement extends HTMLElementBase {
|
|
|
778
778
|
try {
|
|
779
779
|
// if the user has a license then we CAN hide the needle logo
|
|
780
780
|
// calling this method immediately will cause an issue with vite bundling tho
|
|
781
|
-
window.requestAnimationFrame(() =>
|
|
782
|
-
if (res == true &&
|
|
781
|
+
window.requestAnimationFrame(() => qWc(res => {
|
|
782
|
+
if (res == true && Jkdun() && !debugNonCommercial) {
|
|
783
783
|
let visible = this._userRequestedLogoVisible;
|
|
784
784
|
if (visible === undefined) visible = false;
|
|
785
785
|
this.___onSetLogoVisible(visible);
|
|
@@ -830,7 +830,7 @@ export class NeedleMenuElement extends HTMLElementBase {
|
|
|
830
830
|
// ensure the menu is not hidden or removed
|
|
831
831
|
const requiredParent = this?.parentNode;
|
|
832
832
|
if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" || requiredParent != this._domElement?.shadowRoot) {
|
|
833
|
-
if (
|
|
833
|
+
if (!Jkdun()) {
|
|
834
834
|
const change = changeEventCounter++;
|
|
835
835
|
// if a user doesn't have a local pro license *but* for development the menu is hidden then we show a warning
|
|
836
836
|
if (isLocalNetwork() && this._userRequestedMenuVisible === false) {
|
|
@@ -918,7 +918,7 @@ export class NeedleMenuElement extends HTMLElementBase {
|
|
|
918
918
|
showNeedleLogo(visible: boolean) {
|
|
919
919
|
this._userRequestedLogoVisible = visible;
|
|
920
920
|
if (!visible) {
|
|
921
|
-
if (
|
|
921
|
+
if (!Jkdun() || debugNonCommercial) {
|
|
922
922
|
console.warn("[Needle Engine] You need a commercial license to hide the Needle Engine logo in production.");
|
|
923
923
|
const localNetwork = isLocalNetwork()
|
|
924
924
|
if (!localNetwork) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { needleLogoOnlySVG } from "../assets/index.js"
|
|
2
2
|
import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Jkdun, _hyjgv, xWfUNf } from "../engine_license.js";
|
|
4
4
|
import { Mathf } from "../engine_math.js";
|
|
5
5
|
import { LoadingProgressArgs } from "../engine_setup.js";
|
|
6
6
|
import { getParam } from "../engine_utils.js";
|
|
@@ -205,7 +205,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
const hasLicense =
|
|
208
|
+
const hasLicense = _hyjgv();
|
|
209
209
|
if (!existing) {
|
|
210
210
|
this._loadingElement.style.position = "absolute";
|
|
211
211
|
this._loadingElement.style.width = "100%";
|
|
@@ -373,7 +373,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
373
373
|
|
|
374
374
|
// private async handleRuntimeLicense(loadingElement: HTMLElement) {
|
|
375
375
|
// // First check if we have a commercial license
|
|
376
|
-
// let commercialLicense =
|
|
376
|
+
// let commercialLicense = Jkdun();
|
|
377
377
|
// // if it's the case then we don't need to perform a runtime check
|
|
378
378
|
// if (commercialLicense) return;
|
|
379
379
|
|
|
@@ -393,10 +393,10 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
393
393
|
// loadingElement.appendChild(nonCommercialContainer);
|
|
394
394
|
|
|
395
395
|
// // Use the runtime license check
|
|
396
|
-
// if (!isDevEnvironment() &&
|
|
396
|
+
// if (!isDevEnvironment() && xWfUNf) {
|
|
397
397
|
// if (debugLicense) console.log("Waiting for runtime license check");
|
|
398
|
-
// await
|
|
399
|
-
// commercialLicense =
|
|
398
|
+
// await xWfUNf;
|
|
399
|
+
// commercialLicense = Jkdun();
|
|
400
400
|
// }
|
|
401
401
|
// if (commercialLicense) return;
|
|
402
402
|
// nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
|
|
@@ -3,7 +3,7 @@ import type { Texture } from "three";
|
|
|
3
3
|
import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
|
|
4
4
|
import { PUBLIC_KEY, VERSION } from "../engine_constants.js";
|
|
5
5
|
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
|
6
|
-
import {
|
|
6
|
+
import { Jkdun, Telemetry } from "../engine_license.js";
|
|
7
7
|
import { onStart } from "../engine_lifecycle_api.js";
|
|
8
8
|
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "../engine_loaders.gltf.js";
|
|
9
9
|
import { Context, ContextCreateArgs } from "../engine_setup.js";
|
|
@@ -579,7 +579,7 @@ export class NeedleEngineWebComponent extends HTMLElementBase implements INeedle
|
|
|
579
579
|
|
|
580
580
|
|
|
581
581
|
// Loading start events
|
|
582
|
-
const allowOverridingDefaultLoading =
|
|
582
|
+
const allowOverridingDefaultLoading = Jkdun();
|
|
583
583
|
// default loading can be overriden by calling preventDefault in the onload start event
|
|
584
584
|
this.ensureLoadStartIsRegistered();
|
|
585
585
|
let useDefaultLoading = this.dispatchEvent(new CustomEvent("loadstart", {
|
|
@@ -676,6 +676,7 @@ export class NeedleEngineWebComponent extends HTMLElementBase implements INeedle
|
|
|
676
676
|
if (currentHash !== null && currentHash !== undefined)
|
|
677
677
|
context.hash = currentHash;
|
|
678
678
|
context.alias = alias;
|
|
679
|
+
const loadStartTime = performance.now();
|
|
679
680
|
this._createContextPromise = context.create(args);
|
|
680
681
|
const success = await this._createContextPromise;
|
|
681
682
|
this.applyAttributes();
|
|
@@ -694,6 +695,8 @@ export class NeedleEngineWebComponent extends HTMLElementBase implements INeedle
|
|
|
694
695
|
if (useDefaultLoading && success) {
|
|
695
696
|
this._loadingView?.onLoadingUpdate(1, "creating scene");
|
|
696
697
|
}
|
|
698
|
+
// capture before mutating _didFullyLoad below: was a scene already loaded (src swap) vs cold start?
|
|
699
|
+
const isReload = this._didFullyLoad === true;
|
|
697
700
|
this._didFullyLoad = true;
|
|
698
701
|
this.classList.remove("loading");
|
|
699
702
|
this.classList.add("loading-finished");
|
|
@@ -704,6 +707,32 @@ export class NeedleEngineWebComponent extends HTMLElementBase implements INeedle
|
|
|
704
707
|
loadedFiles: loadedFiles,
|
|
705
708
|
}
|
|
706
709
|
}));
|
|
710
|
+
|
|
711
|
+
// Telemetry: report the load outcome. On success we wait for the first rendered frame so we
|
|
712
|
+
// can include time-to-first-frame (and non-identifying device capabilities, collected there).
|
|
713
|
+
const loadDurationMs = performance.now() - loadStartTime;
|
|
714
|
+
if (success) {
|
|
715
|
+
this.addEventListener("ready", () => {
|
|
716
|
+
// ignore if a newer load superseded this one before its first frame arrived
|
|
717
|
+
if (this._loadId !== loadId) return;
|
|
718
|
+
Telemetry.reportLoad(this._context, {
|
|
719
|
+
success: true,
|
|
720
|
+
files: loadedFiles.length,
|
|
721
|
+
is_reload: isReload,
|
|
722
|
+
load_ms: loadDurationMs,
|
|
723
|
+
first_frame_ms: performance.now() - loadStartTime,
|
|
724
|
+
since_navigation_ms: performance.now(),
|
|
725
|
+
});
|
|
726
|
+
}, { once: true });
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
Telemetry.reportLoad(this._context, {
|
|
730
|
+
success: false,
|
|
731
|
+
files: loadedFiles.length,
|
|
732
|
+
is_reload: isReload,
|
|
733
|
+
load_ms: loadDurationMs,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
707
736
|
}
|
|
708
737
|
|
|
709
738
|
// #region applyAttributes
|
|
@@ -53,6 +53,19 @@ class XRUserState {
|
|
|
53
53
|
if (!found) {
|
|
54
54
|
this.controllerStates.push(state);
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
// When switching input type (e.g. hand -> controller), the new input source gets a new index.
|
|
58
|
+
// The old controller's state lingers here (we only ever add/update by index), and a stale
|
|
59
|
+
// non-tracking entry for the same handedness would otherwise mask the new tracked one. Once a
|
|
60
|
+
// tracking state arrives for a handedness, drop any other (stale) states for that handedness.
|
|
61
|
+
if (state.isTracking) {
|
|
62
|
+
for (let i = this.controllerStates.length - 1; i >= 0; i--) {
|
|
63
|
+
const other = this.controllerStates[i];
|
|
64
|
+
if (other.index !== state.index && other.handedness === state.handedness) {
|
|
65
|
+
this.controllerStates.splice(i, 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
update(session: NeedleXRSession) {
|
|
@@ -89,8 +102,11 @@ class XRUserState {
|
|
|
89
102
|
|
|
90
103
|
private sendControllerRemoved(state: XRControllerState) {
|
|
91
104
|
state.isTracking = false;
|
|
92
|
-
state
|
|
105
|
+
// broadcast the not-tracking state to currently connected peers so they hide the hand immediately
|
|
93
106
|
this.context.connection.send(this.userStateEvtName, state);
|
|
107
|
+
// and delete the persisted room state using the *correct* guid so newly joining peers don't
|
|
108
|
+
// replay a stale (still-tracking) controller state. Must use the real guid - clearing it first
|
|
109
|
+
// would leave the persisted entry untouched on the server.
|
|
94
110
|
this.context.connection.sendDeleteRemoteState(state.guid);
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -135,8 +151,10 @@ export class NeedleXRSync {
|
|
|
135
151
|
if (!userId) return undefined;
|
|
136
152
|
const user = this._states.get(userId);
|
|
137
153
|
if (!user) return undefined;
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
// Consider it tracked if ANY controller state for this handedness is tracking. After switching
|
|
155
|
+
// input type a stale non-tracking entry can still exist for a frame before it's pruned - this
|
|
156
|
+
// makes sure it doesn't mask the new tracked controller/hand.
|
|
157
|
+
return user.controllerStates.some(x => x.handedness === handedness && x.isTracking);
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
/** Is it hand tracking or a controller */
|
|
@@ -144,7 +162,9 @@ export class NeedleXRSync {
|
|
|
144
162
|
if (!userId) return undefined;
|
|
145
163
|
const user = this._states.get(userId);
|
|
146
164
|
if (!user) return undefined;
|
|
147
|
-
|
|
165
|
+
// prefer the currently tracking controller for this handedness over any stale entry
|
|
166
|
+
const ctrl = user.controllerStates.find(x => x.handedness === handedness && x.isTracking)
|
|
167
|
+
?? user.controllerStates.find(x => x.handedness === handedness);
|
|
148
168
|
return ctrl?.type || "unknown";
|
|
149
169
|
}
|
|
150
170
|
|
|
@@ -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 { Jkdun } 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 (Jkdun()) {
|
|
219
219
|
const htmlComponent = document.querySelector("needle-engine");
|
|
220
220
|
if (htmlComponent) {
|
|
221
221
|
const licenseLogo = htmlComponent.getAttribute("logo-src");
|
|
@@ -5,7 +5,7 @@ import { InstancingUtil } from "../engine/engine_instancing.js";
|
|
|
5
5
|
import { onUpdate } from '../engine/engine_lifecycle_api.js';
|
|
6
6
|
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
|
|
7
7
|
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
|
|
8
|
-
import { setWorldEuler } from '../engine/engine_three_utils.js';
|
|
8
|
+
import { getTempVector, setWorldEuler } from '../engine/engine_three_utils.js';
|
|
9
9
|
import * as utils from "../engine/engine_utils.js"
|
|
10
10
|
import { registerBinaryType } from '../engine-schemes/schemes.js';
|
|
11
11
|
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
|
|
@@ -34,9 +34,11 @@ export function createTransformModel(guid: string, b: Behaviour, fast: boolean =
|
|
|
34
34
|
SyncedTransformModel.addFast(builder, fast);
|
|
35
35
|
const p = b.worldPosition;
|
|
36
36
|
const r = b.worldEuler;
|
|
37
|
-
const s = b.gameObject.
|
|
37
|
+
const s = b.gameObject.worldScale;
|
|
38
|
+
|
|
38
39
|
// console.log(p, r, s);
|
|
39
40
|
SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
|
|
41
|
+
SyncedTransformModel.addVersion(builder, 1);
|
|
40
42
|
const res = SyncedTransformModel.endSyncedTransformModel(builder);
|
|
41
43
|
// SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
|
|
42
44
|
builder.finish(res, SyncedTransformIdentifier);
|
|
@@ -111,6 +113,9 @@ export class SyncedTransform extends Behaviour {
|
|
|
111
113
|
/** Whether to smoothly interpolate rotation changes when receiving updates */
|
|
112
114
|
public interpolateRotation: boolean = true;
|
|
113
115
|
|
|
116
|
+
/** Whether to smoothly interpolate scale changes when receiving updates */
|
|
117
|
+
public interpolateScale: boolean = true;
|
|
118
|
+
|
|
114
119
|
/** When true, sends updates at a higher frequency, useful for fast-moving objects */
|
|
115
120
|
public fastMode: boolean = false;
|
|
116
121
|
|
|
@@ -120,12 +125,14 @@ export class SyncedTransform extends Behaviour {
|
|
|
120
125
|
// private _state!: SyncedTransformModel;
|
|
121
126
|
private _model: OwnershipModel | null = null;
|
|
122
127
|
private _needsUpdate: boolean = true;
|
|
128
|
+
private _hadOwnershipBeforeDisable: boolean = false;
|
|
123
129
|
private rb: Rigidbody | null = null;
|
|
124
130
|
private _wasKinematic: boolean | undefined = false;
|
|
125
131
|
private _receivedDataBefore: boolean = false;
|
|
126
132
|
|
|
127
133
|
private _targetPosition!: Vector3;
|
|
128
134
|
private _targetRotation!: Quaternion;
|
|
135
|
+
private _targetScale!: Vector3;
|
|
129
136
|
|
|
130
137
|
private _receivedFastUpdate: boolean = false;
|
|
131
138
|
private _shouldRequestOwnership: boolean = false;
|
|
@@ -193,6 +200,7 @@ export class SyncedTransform extends Behaviour {
|
|
|
193
200
|
this._receivedDataBefore = false;
|
|
194
201
|
this._targetPosition = new Vector3();
|
|
195
202
|
this._targetRotation = new Quaternion();
|
|
203
|
+
this._targetScale = new Vector3();
|
|
196
204
|
|
|
197
205
|
// sync instantiate issue was because they shared the same last pos vector!
|
|
198
206
|
this.lastPosition = new Vector3();
|
|
@@ -249,6 +257,7 @@ export class SyncedTransform extends Behaviour {
|
|
|
249
257
|
this.receivedUpdate = true;
|
|
250
258
|
this._receivedFastUpdate = data.fast();
|
|
251
259
|
const transform = data.transform();
|
|
260
|
+
const dataVersion = data.version();
|
|
252
261
|
if (transform) {
|
|
253
262
|
InstancingUtil.markDirty(this.gameObject, true);
|
|
254
263
|
const position = transform.position();
|
|
@@ -271,7 +280,19 @@ export class SyncedTransform extends Behaviour {
|
|
|
271
280
|
|
|
272
281
|
const scale = transform.scale();
|
|
273
282
|
if (scale) {
|
|
274
|
-
|
|
283
|
+
// before dataVersion 1, local scale was sent; from version 1 on world scale is
|
|
284
|
+
// sent (see createTransformModel) and applied in world space so the receiver's
|
|
285
|
+
// local scale is derived relative to its own parent hierarchy
|
|
286
|
+
if (dataVersion === 0) {
|
|
287
|
+
// legacy: local scale, applied directly (no interpolation)
|
|
288
|
+
this.gameObject.scale.set(scale.x(), scale.y(), scale.z());
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
if (this.interpolateScale)
|
|
292
|
+
this._targetScale.set(scale.x(), scale.y(), scale.z());
|
|
293
|
+
if (!this.interpolateScale || !this._receivedDataBefore)
|
|
294
|
+
this.gameObject.worldScale = getTempVector(scale.x(), scale.y(), scale.z());
|
|
295
|
+
}
|
|
275
296
|
}
|
|
276
297
|
}
|
|
277
298
|
this._receivedDataBefore = true;
|
|
@@ -284,26 +305,37 @@ export class SyncedTransform extends Behaviour {
|
|
|
284
305
|
|
|
285
306
|
/**
|
|
286
307
|
* @internal
|
|
287
|
-
* Initializes tracking of position and
|
|
308
|
+
* Initializes tracking of position,rotation and scale when component is enabled
|
|
288
309
|
*/
|
|
289
310
|
onEnable(): void {
|
|
290
311
|
this.lastPosition.copy(this.worldPosition);
|
|
291
312
|
this.lastRotation.copy(this.worldQuaternion);
|
|
292
|
-
this.lastScale.copy(this.gameObject.
|
|
313
|
+
this.lastScale.copy(this.gameObject.worldScale);
|
|
293
314
|
this._needsUpdate = true;
|
|
294
315
|
// console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
|
|
295
316
|
if (this._model) {
|
|
296
317
|
this._model.updateIsOwned();
|
|
318
|
+
// If we owned this before being (transiently) disabled - e.g. the object or an ancestor such
|
|
319
|
+
// as an XRRig got deactivated - reclaim ownership so syncing resumes automatically. We use
|
|
320
|
+
// requestOwnershipIfNotOwned so we only reclaim while it is still free, never stealing it from
|
|
321
|
+
// someone who took it while we were inactive.
|
|
322
|
+
if (this._hadOwnershipBeforeDisable) {
|
|
323
|
+
this._hadOwnershipBeforeDisable = false;
|
|
324
|
+
this._model.requestOwnershipIfNotOwned();
|
|
325
|
+
}
|
|
297
326
|
}
|
|
298
327
|
}
|
|
299
328
|
|
|
300
|
-
/**
|
|
301
|
-
* @internal
|
|
329
|
+
/**
|
|
330
|
+
* @internal
|
|
302
331
|
* Releases ownership when component is disabled
|
|
303
332
|
*/
|
|
304
333
|
onDisable(): void {
|
|
305
|
-
if (this._model)
|
|
334
|
+
if (this._model) {
|
|
335
|
+
// remember whether we were the owner so we can reclaim it on re-enable (see onEnable)
|
|
336
|
+
this._hadOwnershipBeforeDisable = this._model.hasOwnership === true;
|
|
306
337
|
this._model.freeOwnership();
|
|
338
|
+
}
|
|
307
339
|
}
|
|
308
340
|
|
|
309
341
|
|
|
@@ -334,7 +366,7 @@ export class SyncedTransform extends Behaviour {
|
|
|
334
366
|
|
|
335
367
|
const pos = this.worldPosition;
|
|
336
368
|
const rot = this.worldQuaternion;
|
|
337
|
-
const scale = this.gameObject.
|
|
369
|
+
const scale = this.gameObject.worldScale;
|
|
338
370
|
if (this._model.isOwned && !this.receivedUpdate) {
|
|
339
371
|
const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
|
|
340
372
|
if (pos.distanceTo(this.lastPosition) > threshold ||
|
|
@@ -352,7 +384,8 @@ export class SyncedTransform extends Behaviour {
|
|
|
352
384
|
this.worldQuaternion = this.lastRotation;
|
|
353
385
|
rot.copy(this.lastRotation);
|
|
354
386
|
|
|
355
|
-
this.gameObject.
|
|
387
|
+
this.gameObject.worldScale = this.lastScale;
|
|
388
|
+
scale.copy(this.lastScale);
|
|
356
389
|
|
|
357
390
|
InstancingUtil.markDirty(this.gameObject, true);
|
|
358
391
|
this._needsUpdate = false;
|
|
@@ -386,6 +419,12 @@ export class SyncedTransform extends Behaviour {
|
|
|
386
419
|
this.worldQuaternion = rot;
|
|
387
420
|
requireMarkDirty = true;
|
|
388
421
|
}
|
|
422
|
+
if (this.interpolateScale && this._targetScale) {
|
|
423
|
+
const scale = this.gameObject.worldScale;
|
|
424
|
+
scale.lerp(this._targetScale, t);
|
|
425
|
+
this.gameObject.worldScale = scale;
|
|
426
|
+
requireMarkDirty = true;
|
|
427
|
+
}
|
|
389
428
|
if (requireMarkDirty)
|
|
390
429
|
InstancingUtil.markDirty(this.gameObject, true);
|
|
391
430
|
}
|
|
@@ -3,7 +3,7 @@ import { Euler, Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "t
|
|
|
3
3
|
|
|
4
4
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
|
5
5
|
import { findObjectOfType } from "../../../engine/engine_components.js";
|
|
6
|
-
import {
|
|
6
|
+
import { _hyjgv } from "../../../engine/engine_license.js";
|
|
7
7
|
import { serializable } from "../../../engine/engine_serialization.js";
|
|
8
8
|
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
|
|
9
9
|
import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
|
|
@@ -277,7 +277,7 @@ export class USDZExporter extends Behaviour {
|
|
|
277
277
|
let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
|
|
278
278
|
name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
|
|
279
279
|
|
|
280
|
-
if (!
|
|
280
|
+
if (!_hyjgv()) {
|
|
281
281
|
if (name !== "") name += "-";
|
|
282
282
|
name += "MadeWithNeedle";
|
|
283
283
|
}
|
|
@@ -682,7 +682,7 @@ export class USDZExporter extends Behaviour {
|
|
|
682
682
|
if (debug)
|
|
683
683
|
showBalloonMessage("Quicklook url: " + callToActionURL);
|
|
684
684
|
if (callToActionURL) {
|
|
685
|
-
if (!
|
|
685
|
+
if (!_hyjgv()) {
|
|
686
686
|
console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL)
|
|
687
687
|
}
|
|
688
688
|
else {
|
|
@@ -697,7 +697,7 @@ export class USDZExporter extends Behaviour {
|
|
|
697
697
|
private buildQuicklookOverlay(): CustomBranding {
|
|
698
698
|
const obj: CustomBranding = {};
|
|
699
699
|
if (this.customBranding) Object.assign(obj, this.customBranding);
|
|
700
|
-
if (!
|
|
700
|
+
if (!_hyjgv()) {
|
|
701
701
|
console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing");
|
|
702
702
|
obj.callToAction = "Close";
|
|
703
703
|
obj.checkoutTitle = "🌵 Made with Needle";
|
|
@@ -42,6 +42,10 @@ export class Avatar extends Behaviour {
|
|
|
42
42
|
private _leftHandMeshes?: Mesh[];
|
|
43
43
|
private _rightHandMeshes?: Mesh[];
|
|
44
44
|
|
|
45
|
+
// meshes of remote avatar hands - hidden render-only so the hand root stays active and keeps syncing
|
|
46
|
+
private _remoteLeftHandMeshes?: Mesh[];
|
|
47
|
+
private _remoteRightHandMeshes?: Mesh[];
|
|
48
|
+
|
|
45
49
|
private _syncTransforms?: SyncedTransform[];
|
|
46
50
|
|
|
47
51
|
async onEnterXR(_args: NeedleXREventArgs) {
|
|
@@ -120,46 +124,54 @@ export class Avatar extends Behaviour {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
// synchronize hands
|
|
127
|
+
// IMPORTANT: never toggle `.visible` on the hand object itself - it carries the SyncedTransform,
|
|
128
|
+
// and setting it invisible deactivates the component (onDisable -> freeOwnership), which means the
|
|
129
|
+
// owner loses ownership and stops sending (the remote hand then stays frozen). The head syncs fine
|
|
130
|
+
// precisely because its synced root is never deactivated (only a child mesh is hidden). So we keep
|
|
131
|
+
// the hand object active and hide only the rendered meshes (render-only, via setCustomVisibility).
|
|
123
132
|
const leftCtrl = args.xr.leftController;
|
|
124
133
|
const leftObj = this.leftHand?.asset as Object3D;
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
if (leftObj) {
|
|
135
|
+
if (leftCtrl) {
|
|
136
|
+
leftObj.position.copy(leftCtrl.gripPosition);
|
|
137
|
+
leftObj.quaternion.copy(leftCtrl.gripQuaternion);
|
|
138
|
+
leftObj.quaternion.multiply(flipForwardQuaternion);
|
|
139
|
+
}
|
|
140
|
+
leftObj.visible = true;
|
|
130
141
|
this.updateHandVisibility(leftCtrl, leftObj, this._leftHandMeshes);
|
|
131
142
|
}
|
|
132
|
-
else if (leftObj && leftObj.visible) {
|
|
133
|
-
leftObj.visible = false;
|
|
134
|
-
}
|
|
135
143
|
|
|
136
144
|
const right = args.xr.rightController;
|
|
137
145
|
const rightObj = this.rightHand?.asset as Object3D;
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
if (rightObj) {
|
|
147
|
+
if (right) {
|
|
148
|
+
rightObj.position.copy(right.gripPosition);
|
|
149
|
+
rightObj.quaternion.copy(right.gripQuaternion);
|
|
150
|
+
rightObj.quaternion.multiply(flipForwardQuaternion);
|
|
151
|
+
}
|
|
152
|
+
rightObj.visible = true;
|
|
143
153
|
this.updateHandVisibility(right, rightObj, this._rightHandMeshes);
|
|
144
154
|
}
|
|
145
|
-
else if (rightObj && rightObj.visible) {
|
|
146
|
-
rightObj.visible = false;
|
|
147
|
-
}
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
onBeforeRender(): void {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
// Remote avatar visibility depends on the *owner's* tracking state, not on whether THIS client is
|
|
159
|
+
// in XR: other users' avatars are syncInstantiated onto non-XR clients too (spectators / lobby),
|
|
160
|
+
// and must hide/show their hands correctly there as well. So this runs regardless of the local XR
|
|
161
|
+
// state. updateRemoteAvatarVisibility() early-outs for the local player and when not connected.
|
|
162
|
+
if (this.context.time.frame % 10 === 0)
|
|
163
|
+
this.updateRemoteAvatarVisibility();
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
private updateHandVisibility(controller: NeedleXRController, avatarHand: Object3D, meshes: Mesh[] | undefined) {
|
|
166
|
+
private updateHandVisibility(controller: NeedleXRController | null | undefined, avatarHand: Object3D, meshes: Mesh[] | undefined) {
|
|
158
167
|
if (meshes) {
|
|
159
|
-
// Hide the hand meshes
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
// Hide the hand meshes (render-only) when the hand is not tracked, or when another model
|
|
169
|
+
// (e.g. the controller/hand model) is being rendered in its place. We hide the meshes rather
|
|
170
|
+
// than the object's `visible` flag because that would disable SyncedTransform networking.
|
|
171
|
+
const isTracking = controller?.isTracking === true;
|
|
172
|
+
const hasOtherRenderingModel = !!controller?.model && controller.model.visible && controller.model !== avatarHand;
|
|
173
|
+
const render = isTracking && !hasOtherRenderingModel;
|
|
174
|
+
meshes.forEach(mesh => { setCustomVisibility(mesh, render); });
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
|
|
@@ -173,14 +185,13 @@ export class Avatar extends Behaviour {
|
|
|
173
185
|
if (sync.hasState(state.owner)) {
|
|
174
186
|
this.tryFindAvatarObjectsIfMissing();
|
|
175
187
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
188
|
+
// Keep the hand object active (don't toggle `.visible`) so its SyncedTransform keeps
|
|
189
|
+
// applying updates and a non-owner freeOwnership can't strip the owner's ownership.
|
|
190
|
+
// Hide/show the rendered meshes based on the owner's tracking state instead.
|
|
191
|
+
const leftRendered = sync.isTracking(state.owner, "left") ?? false;
|
|
192
|
+
this._remoteLeftHandMeshes = this.setRemoteHandRendered(this.leftHand?.asset as Object3D, this._remoteLeftHandMeshes, leftRendered);
|
|
193
|
+
const rightRendered = sync.isTracking(state.owner, "right") ?? false;
|
|
194
|
+
this._remoteRightHandMeshes = this.setRemoteHandRendered(this.rightHand?.asset as Object3D, this._remoteRightHandMeshes, rightRendered);
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
|
|
@@ -198,6 +209,22 @@ export class Avatar extends Behaviour {
|
|
|
198
209
|
|
|
199
210
|
|
|
200
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Show/hide a remote avatar hand render-only. The hand object stays active so its SyncedTransform keeps
|
|
214
|
+
* applying networked updates (toggling `.visible` would deactivate it and could free the owner's ownership).
|
|
215
|
+
* The mesh list is gathered lazily and cached, and returned so the caller can keep it.
|
|
216
|
+
*/
|
|
217
|
+
private setRemoteHandRendered(obj: Object3D | undefined, meshes: Mesh[] | undefined, rendered: boolean): Mesh[] | undefined {
|
|
218
|
+
if (!obj) return meshes;
|
|
219
|
+
obj.visible = true;
|
|
220
|
+
if (!meshes || meshes.length === 0) {
|
|
221
|
+
meshes = [];
|
|
222
|
+
obj.traverse(o => { if ((o as Mesh).isMesh) meshes!.push(o as Mesh); });
|
|
223
|
+
}
|
|
224
|
+
for (const mesh of meshes) setCustomVisibility(mesh, rendered);
|
|
225
|
+
return meshes;
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
private tryFindAvatarObjectsIfMissing() {
|
|
202
229
|
// if no avatar objects are set, try to find them
|
|
203
230
|
if (!this.head || !this.leftHand || !this.rightHand) {
|
|
@@ -54,8 +54,13 @@ dontSave():boolean {
|
|
|
54
54
|
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
version():number {
|
|
58
|
+
const offset = this.bb!.__offset(this.bb_pos, 12);
|
|
59
|
+
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
static startSyncedTransformModel(builder:flatbuffers.Builder) {
|
|
58
|
-
builder.startObject(
|
|
63
|
+
builder.startObject(5);
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
static addGuid(builder:flatbuffers.Builder, guidOffset:flatbuffers.Offset) {
|
|
@@ -74,6 +79,10 @@ static addDontSave(builder:flatbuffers.Builder, dontSave:boolean) {
|
|
|
74
79
|
builder.addFieldInt8(3, +dontSave, +false);
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
static addVersion(builder:flatbuffers.Builder, version:number) {
|
|
83
|
+
builder.addFieldInt32(4, version, 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
static endSyncedTransformModel(builder:flatbuffers.Builder):flatbuffers.Offset {
|
|
78
87
|
const offset = builder.endObject();
|
|
79
88
|
return offset;
|