@needle-tools/engine 5.0.2 → 5.1.0-canary.87c4c44
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 +24 -0
- package/README.md +6 -7
- package/SKILL.md +39 -21
- package/components.needle.json +1 -1
- package/dist/needle-engine.bundle-BlVD1I5a.min.js +1656 -0
- package/dist/{needle-engine.bundle-BoTyA-Le.js → needle-engine.bundle-CFOipCo_.js} +8519 -7920
- package/dist/needle-engine.bundle-DX2Y-SQF.umd.cjs +1656 -0
- package/dist/needle-engine.d.ts +628 -61
- package/dist/needle-engine.js +575 -565
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{vendor-vHLk8sXu.js → vendor-CAcsI0eU.js} +116 -115
- package/dist/{vendor-CntUvmJu.umd.cjs → vendor-CEM38hLE.umd.cjs} +2 -2
- package/dist/{vendor-DPbfJJ4d.min.js → vendor-HRlxIBga.min.js} +2 -2
- package/lib/engine/api.d.ts +2 -0
- package/lib/engine/api.js +2 -0
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_addressables.js +5 -1
- package/lib/engine/engine_addressables.js.map +1 -1
- package/lib/engine/engine_animation.d.ts +14 -7
- package/lib/engine/engine_animation.js +49 -9
- package/lib/engine/engine_animation.js.map +1 -1
- package/lib/engine/engine_components.js +33 -4
- package/lib/engine/engine_components.js.map +1 -1
- package/lib/engine/engine_context.d.ts +7 -2
- package/lib/engine/engine_context.js +10 -2
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_gameobject.d.ts +4 -0
- package/lib/engine/engine_gameobject.js.map +1 -1
- package/lib/engine/engine_init.js +2 -0
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_input.js +4 -1
- package/lib/engine/engine_input.js.map +1 -1
- package/lib/engine/engine_materialpropertyblock.js +1 -20
- package/lib/engine/engine_materialpropertyblock.js.map +1 -1
- package/lib/engine/engine_networking.d.ts +11 -8
- package/lib/engine/engine_networking.js +43 -26
- package/lib/engine/engine_networking.js.map +1 -1
- package/lib/engine/engine_networking_instantiate.d.ts +100 -5
- package/lib/engine/engine_networking_instantiate.js +150 -16
- package/lib/engine/engine_networking_instantiate.js.map +1 -1
- package/lib/engine/engine_networking_prefabs.d.ts +59 -0
- package/lib/engine/engine_networking_prefabs.js +67 -0
- package/lib/engine/engine_networking_prefabs.js.map +1 -0
- package/lib/engine/postprocessing/api.d.ts +2 -0
- package/lib/engine/postprocessing/api.js +2 -0
- package/lib/engine/postprocessing/api.js.map +1 -0
- package/lib/engine/postprocessing/index.d.ts +2 -0
- package/lib/engine/postprocessing/index.js +2 -0
- package/lib/engine/postprocessing/index.js.map +1 -0
- package/lib/engine/postprocessing/postprocessing.d.ts +83 -0
- package/lib/engine/postprocessing/postprocessing.js +280 -0
- package/lib/engine/postprocessing/postprocessing.js.map +1 -0
- package/lib/engine/postprocessing/types.d.ts +39 -0
- package/lib/engine/postprocessing/types.js +2 -0
- package/lib/engine/postprocessing/types.js.map +1 -0
- package/lib/engine/webcomponents/WebXRButtons.js +17 -3
- package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
- package/lib/engine/webcomponents/icons.js +3 -1
- package/lib/engine/webcomponents/icons.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +1 -0
- package/lib/engine/xr/NeedleXRSession.js +43 -10
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/init.d.ts +4 -0
- package/lib/engine/xr/init.js +49 -0
- package/lib/engine/xr/init.js.map +1 -0
- package/lib/engine-components/AnimationUtils.d.ts +4 -1
- package/lib/engine-components/AnimationUtils.js +7 -19
- package/lib/engine-components/AnimationUtils.js.map +1 -1
- package/lib/engine-components/AnimatorController.d.ts +135 -2
- package/lib/engine-components/AnimatorController.js +216 -13
- package/lib/engine-components/AnimatorController.js.map +1 -1
- package/lib/engine-components/OrbitControls.d.ts +4 -0
- package/lib/engine-components/OrbitControls.js +5 -1
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/SeeThrough.d.ts +0 -2
- package/lib/engine-components/SeeThrough.js +0 -89
- package/lib/engine-components/SeeThrough.js.map +1 -1
- package/lib/engine-components/SyncedRoom.d.ts +4 -0
- package/lib/engine-components/SyncedRoom.js +23 -8
- package/lib/engine-components/SyncedRoom.js.map +1 -1
- package/lib/engine-components/SyncedTransform.js +5 -5
- package/lib/engine-components/SyncedTransform.js.map +1 -1
- package/lib/engine-components/Voip.d.ts +46 -0
- package/lib/engine-components/Voip.js +126 -2
- package/lib/engine-components/Voip.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/postprocessing/Effects/Tonemapping.d.ts +5 -2
- package/lib/engine-components/postprocessing/Effects/Tonemapping.js +11 -18
- package/lib/engine-components/postprocessing/Effects/Tonemapping.js.map +1 -1
- package/lib/engine-components/postprocessing/PostProcessingEffect.d.ts +3 -4
- package/lib/engine-components/postprocessing/PostProcessingEffect.js +6 -15
- package/lib/engine-components/postprocessing/PostProcessingEffect.js.map +1 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +2 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
- package/lib/engine-components/postprocessing/Volume.d.ts +18 -11
- package/lib/engine-components/postprocessing/Volume.js +61 -140
- package/lib/engine-components/postprocessing/Volume.js.map +1 -1
- package/lib/engine-components/postprocessing/index.d.ts +1 -0
- package/lib/engine-components/postprocessing/index.js +1 -0
- package/lib/engine-components/postprocessing/index.js.map +1 -1
- package/lib/engine-components/postprocessing/utils.d.ts +2 -0
- package/lib/engine-components/postprocessing/utils.js +2 -0
- package/lib/engine-components/postprocessing/utils.js.map +1 -1
- package/lib/engine-components/ui/Canvas.js +2 -2
- package/lib/engine-components/ui/Canvas.js.map +1 -1
- package/lib/engine-components/ui/Graphic.d.ts +3 -3
- package/lib/engine-components/ui/Graphic.js +6 -2
- package/lib/engine-components/ui/Graphic.js.map +1 -1
- package/lib/engine-components/ui/Text.d.ts +64 -11
- package/lib/engine-components/ui/Text.js +154 -45
- package/lib/engine-components/ui/Text.js.map +1 -1
- package/lib/engine-components/ui/index.d.ts +1 -0
- package/lib/engine-components/ui/index.js +1 -0
- package/lib/engine-components/ui/index.js.map +1 -1
- package/lib/engine-components-experimental/networking/PlayerSync.d.ts +25 -3
- package/lib/engine-components-experimental/networking/PlayerSync.js +60 -11
- package/lib/engine-components-experimental/networking/PlayerSync.js.map +1 -1
- package/package.json +6 -5
- package/plugins/vite/ai.d.ts +11 -10
- package/plugins/vite/ai.js +305 -31
- package/plugins/vite/dependencies.js +5 -0
- package/src/engine/api.ts +3 -0
- package/src/engine/engine_addressables.ts +4 -1
- package/src/engine/engine_animation.ts +47 -9
- package/src/engine/engine_components.ts +36 -7
- package/src/engine/engine_context.ts +11 -2
- package/src/engine/engine_gameobject.ts +5 -0
- package/src/engine/engine_init.ts +2 -0
- package/src/engine/engine_input.ts +2 -1
- package/src/engine/engine_materialpropertyblock.ts +1 -20
- package/src/engine/engine_networking.ts +46 -23
- package/src/engine/engine_networking_instantiate.ts +160 -18
- package/src/engine/engine_networking_prefabs.ts +80 -0
- package/src/engine/postprocessing/api.ts +2 -0
- package/src/engine/postprocessing/index.ts +2 -0
- package/src/engine/postprocessing/postprocessing.ts +322 -0
- package/src/engine/postprocessing/types.ts +43 -0
- package/src/engine/webcomponents/WebXRButtons.ts +21 -4
- package/src/engine/webcomponents/icons.ts +5 -3
- package/src/engine/xr/NeedleXRSession.ts +50 -15
- package/src/engine/xr/init.ts +56 -0
- package/src/engine-components/AnimationUtils.ts +7 -17
- package/src/engine-components/AnimatorController.ts +288 -18
- package/src/engine-components/OrbitControls.ts +5 -1
- package/src/engine-components/SeeThrough.ts +0 -116
- package/src/engine-components/SyncedRoom.ts +28 -9
- package/src/engine-components/SyncedTransform.ts +5 -5
- package/src/engine-components/Voip.ts +129 -2
- package/src/engine-components/api.ts +1 -0
- package/src/engine-components/postprocessing/Effects/Tonemapping.ts +16 -24
- package/src/engine-components/postprocessing/PostProcessingEffect.ts +9 -16
- package/src/engine-components/postprocessing/PostProcessingHandler.ts +2 -1
- package/src/engine-components/postprocessing/Volume.ts +72 -163
- package/src/engine-components/postprocessing/index.ts +1 -0
- package/src/engine-components/postprocessing/utils.ts +2 -0
- package/src/engine-components/ui/Canvas.ts +2 -2
- package/src/engine-components/ui/Graphic.ts +7 -3
- package/src/engine-components/ui/Text.ts +170 -52
- package/src/engine-components/ui/index.ts +2 -1
- package/src/engine-components-experimental/networking/PlayerSync.ts +64 -11
- package/dist/needle-engine.bundle-B3ywqx5o.min.js +0 -1654
- package/dist/needle-engine.bundle-CzOPcOui.umd.cjs +0 -1654
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { EffectComposer } from "postprocessing";
|
|
2
|
+
import type { ToneMapping } from "three";
|
|
3
|
+
import type { EffectComposer as ThreeEffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal interface for a postprocessing effect as seen by the core stack.
|
|
7
|
+
* Implemented by `PostProcessingEffect` in engine-components.
|
|
8
|
+
*/
|
|
9
|
+
export interface IPostProcessingEffect {
|
|
10
|
+
readonly active: boolean;
|
|
11
|
+
readonly enabled: boolean;
|
|
12
|
+
/** When true, this effect is a tonemapping effect. The core stack uses this to detect tonemapping-only scenarios. */
|
|
13
|
+
readonly isToneMapping?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extended interface for tonemapping effects.
|
|
18
|
+
* When ONLY tonemapping effects are in the stack, the core applies tonemapping
|
|
19
|
+
* directly to the renderer instead of creating a full postprocessing pipeline.
|
|
20
|
+
*/
|
|
21
|
+
export interface ITonemappingEffect extends IPostProcessingEffect {
|
|
22
|
+
readonly isToneMapping: true;
|
|
23
|
+
/** The three.js ToneMapping enum value to apply to the renderer */
|
|
24
|
+
readonly threeToneMapping: ToneMapping;
|
|
25
|
+
/** The exposure value to apply to the renderer */
|
|
26
|
+
readonly toneMappingExposure: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Interface for the pipeline builder that manages the EffectComposer.
|
|
31
|
+
* Implemented by `PostProcessingHandler` in engine-components.
|
|
32
|
+
*/
|
|
33
|
+
export interface IPostProcessingHandler {
|
|
34
|
+
readonly composer: EffectComposer | ThreeEffectComposer | null;
|
|
35
|
+
readonly hasSmaaEffect: boolean;
|
|
36
|
+
multisampling: number;
|
|
37
|
+
adaptivePixelRatio: boolean;
|
|
38
|
+
|
|
39
|
+
apply(effects: IPostProcessingEffect[]): Promise<void>;
|
|
40
|
+
unapply(dispose?: boolean): void;
|
|
41
|
+
updateAdaptivePixelRatio(): void;
|
|
42
|
+
dispose(): void;
|
|
43
|
+
}
|
|
@@ -6,7 +6,7 @@ import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
|
|
|
6
6
|
import { ButtonsFactory } from "./buttons.js";
|
|
7
7
|
import { getIconElement } from "./icons.js";
|
|
8
8
|
import { NeedleMenu } from "./needle menu/needle-menu.js";
|
|
9
|
-
import { getOrCreateQuicklookHandler,type IQuicklookHandler } from "./quicklook-handler.js";
|
|
9
|
+
import { getOrCreateQuicklookHandler, type IQuicklookHandler } from "./quicklook-handler.js";
|
|
10
10
|
|
|
11
11
|
// TODO: move these buttons into their own web components so their logic is encapsulated (e.g. the CSS animation when a xr session is requested)
|
|
12
12
|
|
|
@@ -120,10 +120,27 @@ export class WebXRButtonFactory {
|
|
|
120
120
|
|
|
121
121
|
button.classList.add("webxr-button");
|
|
122
122
|
button.dataset["needle"] = "webxr-ar-button";
|
|
123
|
-
button.innerText = "Enter AR";
|
|
124
|
-
button.prepend(getIconElement("view_in_ar"))
|
|
125
123
|
button.title = "Click to start an AR session";
|
|
126
|
-
|
|
124
|
+
const setDefaultText = () => {
|
|
125
|
+
button.innerText = "Enter AR";
|
|
126
|
+
button.prepend(getIconElement("view_in_ar"));
|
|
127
|
+
};
|
|
128
|
+
setDefaultText();
|
|
129
|
+
|
|
130
|
+
let tapAgainTimeout: ReturnType<typeof setTimeout>;
|
|
131
|
+
button.addEventListener("click", () => {
|
|
132
|
+
|
|
133
|
+
NeedleXRSession.start(mode, init);
|
|
134
|
+
|
|
135
|
+
if (DeviceUtilities.isiOS() && !DeviceUtilities.isVisionOS()) {
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
button.innerHTML = "Tap again";
|
|
138
|
+
button.prepend(getIconElement("view_in_ar"));
|
|
139
|
+
clearTimeout(tapAgainTimeout);
|
|
140
|
+
tapAgainTimeout = setTimeout(setDefaultText, 4000);
|
|
141
|
+
}, 500);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
127
144
|
NeedleMenu.setElementPriority(button, arButtonPriority);
|
|
128
145
|
this.updateSessionSupported(button, mode);
|
|
129
146
|
this.listenToXRSessionState(button, mode);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Texture } from "three";
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
const fontname = "Material Symbols Outlined";
|
|
5
4
|
|
|
6
5
|
/** Returns a HTML element containing an icon. Using https://fonts.google.com/icons
|
|
@@ -21,7 +20,10 @@ export function getIconElement(str: string): HTMLElement {
|
|
|
21
20
|
span.setAttribute("aria-label", str + " icon");
|
|
22
21
|
span.setAttribute("aria-hidden", "true");
|
|
23
22
|
fontReady(fontname).then(res => {
|
|
24
|
-
if (res)
|
|
23
|
+
if (res) {
|
|
24
|
+
span.style.visibility = "";
|
|
25
|
+
span.innerText = str; // re-assign, otherise more_vert doesnt show up. Maybe because of layout restructuring on mobile
|
|
26
|
+
}
|
|
25
27
|
else {
|
|
26
28
|
if (str === "more_vert") {
|
|
27
29
|
span.style.visibility = "";
|
|
@@ -31,7 +33,7 @@ export function getIconElement(str: string): HTMLElement {
|
|
|
31
33
|
span.style.display = "none";
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
|
-
})
|
|
36
|
+
});
|
|
35
37
|
return span;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -79,13 +79,9 @@ function getDOMOverlayElement(domElement: HTMLElement) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
|
|
83
82
|
handleSessionGranted();
|
|
84
83
|
async function handleSessionGranted() {
|
|
85
84
|
|
|
86
|
-
// await delay(400);
|
|
87
|
-
|
|
88
|
-
|
|
89
85
|
let defaultMode: XRSessionMode = "immersive-vr";
|
|
90
86
|
|
|
91
87
|
try {
|
|
@@ -109,8 +105,6 @@ async function handleSessionGranted() {
|
|
|
109
105
|
return;
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
// showBalloonMessage("sessiongranted: " + defaultMode);
|
|
113
|
-
|
|
114
108
|
// TODO: asap session granted doesnt handle the pre-room yet
|
|
115
109
|
if (getParam("debugasap")) {
|
|
116
110
|
let asapSession = globalThis["needle:XRSession"] as XRSession | undefined | Promise<XRSession>;
|
|
@@ -149,7 +143,6 @@ async function handleSessionGranted() {
|
|
|
149
143
|
navigator.xr?.addEventListener('sessiongranted', async () => {
|
|
150
144
|
// enableSpatialConsole(true);
|
|
151
145
|
|
|
152
|
-
|
|
153
146
|
const lastSessionMode = sessionStorage.getItem("needle_xr_session_mode") as XRSessionMode;
|
|
154
147
|
const lastSessionInit = sessionStorage.getItem("needle_xr_session_init") ?? null;
|
|
155
148
|
const init = lastSessionInit ? JSON.parse(lastSessionInit) : null;
|
|
@@ -331,7 +324,18 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
331
324
|
* @param mode The XRSessionMode to check if it is supported
|
|
332
325
|
* @returns true if the browser supports the given XRSessionMode
|
|
333
326
|
*/
|
|
334
|
-
static isSessionSupported(mode: XRSessionMode) {
|
|
327
|
+
static isSessionSupported(mode: XRSessionMode) {
|
|
328
|
+
// We cache the result of the session support check to avoid unnecessary calls to isSessionSupported. This is especially important for user input events that internally run session support checks during onclick where the event must be resolved as fast as possible.
|
|
329
|
+
if (this._sessionSupportedCache[mode] !== undefined) {
|
|
330
|
+
return Promise.resolve(this._sessionSupportedCache[mode]);
|
|
331
|
+
}
|
|
332
|
+
return this.xrSystem?.isSessionSupported(mode).then(supported => {
|
|
333
|
+
this._sessionSupportedCache[mode] = supported;
|
|
334
|
+
return supported;
|
|
335
|
+
}).catch(err => { if (debug) console.error(err); return false }) ?? Promise.resolve(false);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private static _sessionSupportedCache: { [mode in XRSessionMode]?: boolean } = {};
|
|
335
339
|
|
|
336
340
|
private static _currentSessionRequest?: Promise<XRSession>;
|
|
337
341
|
private static _activeSession: NeedleXRSession | null;
|
|
@@ -466,10 +470,11 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
466
470
|
// handle iOS platform where "immersive-ar" is special:
|
|
467
471
|
// - we either launch QuickLook
|
|
468
472
|
// - or forward to the Needle App Clip experience for WebXR AR
|
|
469
|
-
// TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
|
|
470
473
|
if (DeviceUtilities.isiOS()) {
|
|
471
474
|
|
|
472
|
-
|
|
475
|
+
// IMPORTANT: on iOS we should have prefetched immersive-ar support already on app start (this is done in xrInit()) - if not then clicking the Enter AR button for the very first time does not immediately open the AppClip overlay.
|
|
476
|
+
// Without an async await here the overlay *does* however open even when clicking for the very first time. (To test this you need to clear the local experience cache on device)
|
|
477
|
+
const arSupported = this._sessionSupportedCache["immersive-ar"] ?? await this.isARSupported();
|
|
473
478
|
|
|
474
479
|
// On VisionOS, we use QuickLook for AR experiences; no AppClip support for now.
|
|
475
480
|
if (DeviceUtilities.isVisionOS() && !arSupported && (mode === "ar" || mode === "immersive-ar")) {
|
|
@@ -489,11 +494,14 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
489
494
|
|
|
490
495
|
this.invokeSessionRequestStart("immersive-ar", init);
|
|
491
496
|
|
|
497
|
+
// if we are in an iframe, we need to navigate the top window
|
|
498
|
+
const topWindow = window.top || window;
|
|
499
|
+
const originalUrl = topWindow.location.href;
|
|
500
|
+
|
|
492
501
|
// Forward to the AppClip experience (Using the apple.com url the appclip overlay shows immediately)
|
|
493
502
|
// const url =`https://appclip.needle.tools/ar?url=${(location.href)}`;
|
|
494
503
|
const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
|
|
495
504
|
url.searchParams.set("url", location.href);
|
|
496
|
-
|
|
497
505
|
const urlStr = url.toString();
|
|
498
506
|
|
|
499
507
|
Telemetry.sendEvent(Context.Current, "xr", {
|
|
@@ -502,16 +510,43 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
502
510
|
url: urlStr,
|
|
503
511
|
});
|
|
504
512
|
|
|
505
|
-
// if we are in an iframe, we need to navigate the top window
|
|
506
|
-
const topWindow = window.top || window;
|
|
507
513
|
try {
|
|
508
514
|
console.debug("iOS device detected - opening Needle App Clip for AR experience", { mode, init, url });
|
|
515
|
+
|
|
516
|
+
// Prewarm the appclip by opening the URL
|
|
517
|
+
// BUT on safari triggering this with multiple URLs causes the
|
|
518
|
+
// Browser to show the dialogue AGAIN in the background
|
|
519
|
+
// SO: we don't do it here...
|
|
520
|
+
const key = "appclip-prewarm:" + new URL(url).origin;
|
|
521
|
+
const allowMultipleNavigations = sessionStorage.getItem(key) === null;
|
|
522
|
+
if (allowMultipleNavigations) {
|
|
523
|
+
const secondUrl = new URL(url);
|
|
524
|
+
secondUrl.searchParams.set("prewarm", "1");
|
|
525
|
+
// eslint-disable-next-line xss/no-location-href-assign
|
|
526
|
+
topWindow.location.href = secondUrl.toString();
|
|
527
|
+
|
|
528
|
+
// wait a bit to give the device time to fetch the app clip metadata - this seems to fix the double tap issue (500 ms seem to work)
|
|
529
|
+
await new Promise(res => setTimeout(res, 300));
|
|
530
|
+
}
|
|
531
|
+
|
|
509
532
|
// navigate to app clip url but keep the current url in history, open in same tab
|
|
510
533
|
// eslint-disable-next-line xss/no-location-href-assign
|
|
511
534
|
topWindow.location.href = urlStr;
|
|
535
|
+
|
|
536
|
+
if (allowMultipleNavigations && !DeviceUtilities.isSafari()) {
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
const url2 = new URL(url);
|
|
539
|
+
url2.searchParams.set("prewarm", "1");
|
|
540
|
+
// eslint-disable-next-line xss/no-location-href-assign
|
|
541
|
+
topWindow.location.href = url2.toString();
|
|
542
|
+
}, 500);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
sessionStorage.setItem(key, "1");
|
|
546
|
+
|
|
512
547
|
}
|
|
513
548
|
catch (e) {
|
|
514
|
-
console.warn(
|
|
549
|
+
console.warn(`Error navigating to AppClip ${urlStr}\n`, e);
|
|
515
550
|
// if top window navigation fails and we are in an iframe, we try to navigate the top window directly
|
|
516
551
|
const weAreInIframe = window !== window.top;
|
|
517
552
|
if (weAreInIframe) {
|
|
@@ -531,7 +566,7 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
531
566
|
}
|
|
532
567
|
|
|
533
568
|
if (mode === "quicklook") {
|
|
534
|
-
console.
|
|
569
|
+
console.error("QuickLook mode is only supported on iOS devices.");
|
|
535
570
|
return null;
|
|
536
571
|
}
|
|
537
572
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isDevEnvironment, showBalloonMessage } from "../debug/index.js";
|
|
2
|
+
import { DeviceUtilities, setParamWithoutReload } from "../engine_utils.js";
|
|
3
|
+
import { NeedleXRSession } from "./NeedleXRSession.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize XR subsystem. Called from `initEngine()`
|
|
7
|
+
*/
|
|
8
|
+
export function initXR() {
|
|
9
|
+
// Prewarm AR support check
|
|
10
|
+
NeedleXRSession.isARSupported();
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if (DeviceUtilities.isiOS()) {
|
|
15
|
+
|
|
16
|
+
if (isDevEnvironment()) {
|
|
17
|
+
const randomParameterValue = Date.now().toString();
|
|
18
|
+
setParamWithoutReload("debug_appclip", randomParameterValue);
|
|
19
|
+
showBalloonMessage("iOS appclip debug: " + randomParameterValue);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Prefetch
|
|
23
|
+
const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
|
|
24
|
+
url.searchParams.set("url", location.href);
|
|
25
|
+
const urlStr = url.toString();
|
|
26
|
+
fetch(urlStr, { method: "HEAD", mode: "no-cors" }).catch(() => {
|
|
27
|
+
// appclip prefetch - to get metadata faster on iOS devices, this seems to fix the double tap issue when opening the appclip for AR sessions.
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// We add the meta tag here to preload app clip card data for iOS.
|
|
32
|
+
const meta = window.top?.document.querySelector('meta[name="apple-itunes-app"]');
|
|
33
|
+
if(!meta) {
|
|
34
|
+
const metaTag = document.createElement("meta");
|
|
35
|
+
metaTag.name = "apple-itunes-app";
|
|
36
|
+
metaTag.content = "app-id=6757205152, app-clip-bundle-id=tools.needle.launch-app.Clip, app-clip-display=card";
|
|
37
|
+
window.top?.document.head.appendChild(metaTag);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.warn("Error adding apple-itunes-app meta tag for appclip support\n", e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// We preconnect to apple here to speed up the appclip meta request for the first click. NOT sure if necessary and working but can't hurt either?
|
|
46
|
+
const topWindow = window.top || window;
|
|
47
|
+
const preconnectMeta = topWindow.document.createElement("link");
|
|
48
|
+
preconnectMeta.rel = "preconnect";
|
|
49
|
+
preconnectMeta.href = url.toString();
|
|
50
|
+
topWindow.document.head.appendChild(preconnectMeta);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.warn("Error adding preconnect link for appclip.apple.com\n", e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
import { Object3D } from "three";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { AnimationUtils } from "../engine/engine_animation.js";
|
|
4
4
|
|
|
5
5
|
/** Internal method - This marks an object as being animated. Make sure to always call isAnimated=false if you stop animating the object
|
|
6
6
|
* @param obj The object to mark
|
|
7
7
|
* @param isAnimated Whether the object is animated or not
|
|
8
|
+
* @deprecated Use {@link AnimationUtils.setObjectAnimated} instead
|
|
8
9
|
*/
|
|
9
10
|
export function setObjectAnimated(obj: Object3D, animatedBy: object, isAnimated: boolean) {
|
|
10
|
-
|
|
11
|
-
if (obj[$objectAnimationKey] === undefined) {
|
|
12
|
-
if (!isAnimated) return;
|
|
13
|
-
obj[$objectAnimationKey] = new Set<object>();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const set = obj[$objectAnimationKey] as Set<object>;
|
|
17
|
-
if (isAnimated) {
|
|
18
|
-
set.add(animatedBy);
|
|
19
|
-
}
|
|
20
|
-
else if (set.has(animatedBy))
|
|
21
|
-
set.delete(animatedBy);
|
|
11
|
+
return AnimationUtils.setObjectAnimated(obj, animatedBy, isAnimated);
|
|
22
12
|
}
|
|
23
13
|
|
|
24
|
-
/** Get is the object is currently animated. Currently used by the Animator to check if a timeline animationtrack is actively animating an object
|
|
14
|
+
/** Get is the object is currently animated. Currently used by the Animator to check if a timeline animationtrack is actively animating an object
|
|
15
|
+
* @deprecated Use {@link AnimationUtils.getObjectAnimated} instead
|
|
16
|
+
*/
|
|
25
17
|
export function getObjectAnimated(obj: Object3D): boolean {
|
|
26
|
-
|
|
27
|
-
const set = obj[$objectAnimationKey] as Set<object>;
|
|
28
|
-
return set !== undefined && set.size > 0;
|
|
18
|
+
return AnimationUtils.getObjectAnimated(obj) || false;
|
|
29
19
|
}
|
|
@@ -9,27 +9,13 @@ import { Context } from "../engine/engine_setup.js";
|
|
|
9
9
|
import { isAnimationAction } from "../engine/engine_three_utils.js";
|
|
10
10
|
import { TypeStore } from "../engine/engine_typestore.js";
|
|
11
11
|
import { deepClone, getParam } from "../engine/engine_utils.js";
|
|
12
|
-
import type { AnimatorControllerModel, Condition, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
|
12
|
+
import type { AnimatorControllerModel, Condition, Parameter, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
|
13
13
|
import { AnimatorConditionMode, AnimatorControllerParameterType, AnimatorStateInfo, createMotion, StateMachineBehaviour } from "../engine/extensions/NEEDLE_animator_controller_model.js";
|
|
14
14
|
import type { Animator } from "./Animator.js";
|
|
15
15
|
|
|
16
16
|
const debug = getParam("debuganimatorcontroller");
|
|
17
17
|
const debugRootMotion = getParam("debugrootmotion");
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Generates a hash code for a string
|
|
21
|
-
* @param str - The string to hash
|
|
22
|
-
* @returns A numeric hash value
|
|
23
|
-
*/
|
|
24
|
-
function stringToHash(str): number {
|
|
25
|
-
let hash = 0;
|
|
26
|
-
for (let i = 0; i < str.length; i++) {
|
|
27
|
-
const char = str.charCodeAt(i);
|
|
28
|
-
hash = ((hash << 5) - hash) + char;
|
|
29
|
-
hash = hash & hash;
|
|
30
|
-
}
|
|
31
|
-
return hash;
|
|
32
|
-
}
|
|
33
19
|
|
|
34
20
|
/**
|
|
35
21
|
* Configuration options for creating an AnimatorController
|
|
@@ -43,15 +29,275 @@ declare type CreateAnimatorControllerOptions = {
|
|
|
43
29
|
transitionDuration?: number,
|
|
44
30
|
}
|
|
45
31
|
|
|
46
|
-
|
|
32
|
+
|
|
33
|
+
// #region AnimatorControllerBuilder
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for an animation state in the builder
|
|
37
|
+
*/
|
|
38
|
+
export declare type StateOptions = {
|
|
39
|
+
/** The animation clip for this state */
|
|
40
|
+
clip: AnimationClip;
|
|
41
|
+
/** Whether the animation should loop (default: false) */
|
|
42
|
+
loop?: boolean;
|
|
43
|
+
/** Base speed multiplier (default: 1) */
|
|
44
|
+
speed?: number;
|
|
45
|
+
/** Name of a float parameter to multiply with speed */
|
|
46
|
+
speedParameter?: string;
|
|
47
|
+
/** Normalized cycle offset 0-1 (default: 0) */
|
|
48
|
+
cycleOffset?: number;
|
|
49
|
+
/** Name of a float parameter to use as cycle offset */
|
|
50
|
+
cycleOffsetParameter?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configuration for a transition in the builder
|
|
55
|
+
*/
|
|
56
|
+
export declare type TransitionOptions = {
|
|
57
|
+
/** Duration of the crossfade in seconds (default: 0) */
|
|
58
|
+
duration?: number;
|
|
59
|
+
/** Normalized exit time 0-1 (default: 1). Only used when hasExitTime is true */
|
|
60
|
+
exitTime?: number;
|
|
61
|
+
/** Whether the transition waits for exitTime before transitioning (default: false) */
|
|
62
|
+
hasExitTime?: boolean;
|
|
63
|
+
/** Normalized offset into the destination state's animation (default: 0) */
|
|
64
|
+
offset?: number;
|
|
65
|
+
/** Whether duration is in seconds (true) or normalized (false) (default: false) */
|
|
66
|
+
hasFixedDuration?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** String condition modes for the builder, mapped to {@link AnimatorConditionMode} */
|
|
70
|
+
export type ConditionMode = "if" | "ifNot" | "greater" | "less" | "equals" | "notEqual";
|
|
71
|
+
|
|
72
|
+
function conditionModeToEnum(mode: ConditionMode): AnimatorConditionMode {
|
|
73
|
+
switch (mode) {
|
|
74
|
+
case "if": return AnimatorConditionMode.If;
|
|
75
|
+
case "ifNot": return AnimatorConditionMode.IfNot;
|
|
76
|
+
case "greater": return AnimatorConditionMode.Greater;
|
|
77
|
+
case "less": return AnimatorConditionMode.Less;
|
|
78
|
+
case "equals": return AnimatorConditionMode.Equals;
|
|
79
|
+
case "notEqual": return AnimatorConditionMode.NotEqual;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type BuilderTransition = {
|
|
84
|
+
to: string;
|
|
85
|
+
options: TransitionOptions;
|
|
86
|
+
conditions: Array<{ parameter: string; mode: ConditionMode; threshold: number }>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type BuilderState = {
|
|
90
|
+
name: string;
|
|
91
|
+
options: StateOptions;
|
|
92
|
+
transitions: BuilderTransition[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A fluent builder for creating {@link AnimatorController} instances from code.
|
|
97
|
+
*
|
|
98
|
+
* Use {@link AnimatorController.build} to create a new builder.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const controller = AnimatorController.build("CharacterController")
|
|
103
|
+
* .floatParameter("Speed", 0)
|
|
104
|
+
* .triggerParameter("Jump")
|
|
105
|
+
* .state("Idle", { clip: idleClip, loop: true })
|
|
106
|
+
* .state("Walk", { clip: walkClip, loop: true })
|
|
107
|
+
* .state("Jump", { clip: jumpClip })
|
|
108
|
+
* .transition("Idle", "Walk", { duration: 0.25 })
|
|
109
|
+
* .condition("Speed", "greater", 0.1)
|
|
110
|
+
* .transition("Walk", "Idle", { duration: 0.25 })
|
|
111
|
+
* .condition("Speed", "less", 0.1)
|
|
112
|
+
* .transition("*", "Jump", { duration: 0.1 })
|
|
113
|
+
* .condition("Jump", "if")
|
|
114
|
+
* .transition("Jump", "Idle", { hasExitTime: true, exitTime: 0.9, duration: 0.25 })
|
|
115
|
+
* .build();
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @category Animation and Sequencing
|
|
119
|
+
* @group Utilities
|
|
120
|
+
*/
|
|
121
|
+
export class AnimatorControllerBuilder {
|
|
122
|
+
private _name: string;
|
|
123
|
+
private _parameters: Parameter[] = [];
|
|
124
|
+
private _states: BuilderState[] = [];
|
|
125
|
+
private _anyStateTransitions: BuilderTransition[] = [];
|
|
126
|
+
private _defaultStateName: string | null = null;
|
|
127
|
+
private _lastTransition: BuilderTransition | null = null;
|
|
128
|
+
|
|
129
|
+
constructor(name?: string) {
|
|
130
|
+
this._name = name ?? "AnimatorController";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Adds a float parameter */
|
|
134
|
+
floatParameter(name: string, defaultValue: number = 0): this {
|
|
135
|
+
this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Float, value: defaultValue });
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Adds an integer parameter */
|
|
140
|
+
intParameter(name: string, defaultValue: number = 0): this {
|
|
141
|
+
this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Int, value: defaultValue });
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Adds a boolean parameter */
|
|
146
|
+
boolParameter(name: string, defaultValue: boolean = false): this {
|
|
147
|
+
this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Bool, value: defaultValue });
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Adds a trigger parameter */
|
|
152
|
+
triggerParameter(name: string): this {
|
|
153
|
+
this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Trigger, value: false });
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Adds a state to the controller. The first state added becomes the default state.
|
|
159
|
+
* @param name - Unique name for the state
|
|
160
|
+
* @param options - State configuration including clip, loop, speed
|
|
161
|
+
*/
|
|
162
|
+
state(name: string, options: StateOptions): this {
|
|
163
|
+
this._states.push({ name, options, transitions: [] });
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Adds a transition between two states.
|
|
169
|
+
* Use `"*"` as the source to create a transition from any state.
|
|
170
|
+
* Chain `.condition()` calls after this to add conditions.
|
|
171
|
+
* @param from - Source state name, or `"*"` for any-state transition
|
|
172
|
+
* @param to - Destination state name
|
|
173
|
+
* @param options - Transition configuration
|
|
174
|
+
*/
|
|
175
|
+
transition(from: string, to: string, options?: TransitionOptions): this {
|
|
176
|
+
const t: BuilderTransition = { to, options: options ?? {}, conditions: [] };
|
|
177
|
+
if (from === "*") {
|
|
178
|
+
this._anyStateTransitions.push(t);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const state = this._states.find(s => s.name === from);
|
|
182
|
+
if (!state) throw new Error(`AnimatorControllerBuilder: source state "${from}" not found. Add it with .state() first.`);
|
|
183
|
+
state.transitions.push(t);
|
|
184
|
+
}
|
|
185
|
+
this._lastTransition = t;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Adds a condition to the most recently added transition.
|
|
191
|
+
* Multiple conditions on the same transition are AND-ed together.
|
|
192
|
+
* @param parameter - Name of the parameter to evaluate
|
|
193
|
+
* @param mode - Condition mode: `"if"`, `"ifNot"`, `"greater"`, `"less"`, `"equals"`, `"notEqual"`
|
|
194
|
+
* @param threshold - Comparison threshold for numeric conditions (default: 0)
|
|
195
|
+
*/
|
|
196
|
+
condition(parameter: string, mode: ConditionMode, threshold: number = 0): this {
|
|
197
|
+
if (!this._lastTransition) throw new Error("AnimatorControllerBuilder: .condition() must be called after .transition()");
|
|
198
|
+
this._lastTransition.conditions.push({ parameter, mode, threshold });
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Sets which state is the default/entry state.
|
|
204
|
+
* If not called, the first added state is used.
|
|
205
|
+
* @param name - Name of the state
|
|
206
|
+
*/
|
|
207
|
+
defaultState(name: string): this {
|
|
208
|
+
this._defaultStateName = name;
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds and returns the {@link AnimatorController}.
|
|
214
|
+
* Resolves all state name references to indices.
|
|
215
|
+
*/
|
|
216
|
+
build(): AnimatorController {
|
|
217
|
+
const stateIndexMap = new Map<string, number>();
|
|
218
|
+
this._states.forEach((s, i) => stateIndexMap.set(s.name, i));
|
|
219
|
+
|
|
220
|
+
let defaultStateIndex = 0;
|
|
221
|
+
if (this._defaultStateName !== null) {
|
|
222
|
+
const idx = stateIndexMap.get(this._defaultStateName);
|
|
223
|
+
if (idx === undefined) throw new Error(`AnimatorControllerBuilder: default state "${this._defaultStateName}" not found`);
|
|
224
|
+
defaultStateIndex = idx;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const resolveTransition = (t: BuilderTransition): Transition => {
|
|
228
|
+
const destIndex = stateIndexMap.get(t.to);
|
|
229
|
+
if (destIndex === undefined) throw new Error(`AnimatorControllerBuilder: transition target "${t.to}" not found`);
|
|
230
|
+
return {
|
|
231
|
+
exitTime: t.options.exitTime ?? 1,
|
|
232
|
+
hasExitTime: t.options.hasExitTime ?? false,
|
|
233
|
+
duration: t.options.duration ?? 0,
|
|
234
|
+
offset: t.options.offset ?? 0,
|
|
235
|
+
hasFixedDuration: t.options.hasFixedDuration ?? false,
|
|
236
|
+
destinationState: destIndex,
|
|
237
|
+
conditions: t.conditions.map(c => ({
|
|
238
|
+
parameter: c.parameter,
|
|
239
|
+
mode: conditionModeToEnum(c.mode),
|
|
240
|
+
threshold: c.threshold,
|
|
241
|
+
})),
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const states: State[] = this._states.map((s, index) => {
|
|
246
|
+
const transitions: Transition[] = s.transitions.map(resolveTransition);
|
|
247
|
+
|
|
248
|
+
// Replicate any-state transitions onto every state (except self-targeting)
|
|
249
|
+
for (const anyT of this._anyStateTransitions) {
|
|
250
|
+
const destIndex = stateIndexMap.get(anyT.to);
|
|
251
|
+
if (destIndex === index) continue;
|
|
252
|
+
transitions.push(resolveTransition(anyT));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
name: s.name,
|
|
257
|
+
hash: index,
|
|
258
|
+
motion: {
|
|
259
|
+
name: s.options.clip.name,
|
|
260
|
+
clip: s.options.clip,
|
|
261
|
+
isLooping: s.options.loop ?? false,
|
|
262
|
+
},
|
|
263
|
+
transitions,
|
|
264
|
+
behaviours: [],
|
|
265
|
+
speed: s.options.speed,
|
|
266
|
+
speedParameter: s.options.speedParameter,
|
|
267
|
+
cycleOffset: s.options.cycleOffset,
|
|
268
|
+
cycleOffsetParameter: s.options.cycleOffsetParameter,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const model: AnimatorControllerModel = {
|
|
273
|
+
name: this._name,
|
|
274
|
+
guid: new InstantiateIdProvider(Date.now()).generateUUID(),
|
|
275
|
+
parameters: this._parameters,
|
|
276
|
+
layers: [{
|
|
277
|
+
name: "Base Layer",
|
|
278
|
+
stateMachine: {
|
|
279
|
+
defaultState: defaultStateIndex,
|
|
280
|
+
states,
|
|
281
|
+
}
|
|
282
|
+
}],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return new AnimatorController(model);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// #endregion
|
|
289
|
+
|
|
290
|
+
// #region AnimatorController
|
|
291
|
+
/**
|
|
47
292
|
* Controls the playback of animations using a state machine architecture.
|
|
48
293
|
*
|
|
49
294
|
* The AnimatorController manages animation states, transitions between states,
|
|
50
295
|
* and parameters that affect those transitions. It is used by the {@link Animator}
|
|
51
296
|
* component to control animation behavior on 3D models.
|
|
52
297
|
*
|
|
53
|
-
* Use
|
|
54
|
-
*
|
|
298
|
+
* Use {@link AnimatorController.build} to fluently create a controller with parameters,
|
|
299
|
+
* states, transitions, and conditions. For simple sequential playback,
|
|
300
|
+
* use {@link AnimatorController.createFromClips}.
|
|
55
301
|
*
|
|
56
302
|
* @category Animation and Sequencing
|
|
57
303
|
* @group Utilities
|
|
@@ -119,6 +365,30 @@ export class AnimatorController {
|
|
|
119
365
|
return controller;
|
|
120
366
|
}
|
|
121
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Creates a new {@link AnimatorControllerBuilder} for fluently constructing a controller with
|
|
370
|
+
* parameters, states, transitions, and conditions.
|
|
371
|
+
*
|
|
372
|
+
* @param name - Optional name for the controller
|
|
373
|
+
* @returns A new builder instance
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```ts
|
|
377
|
+
* const ctrl = AnimatorController.build("MyController")
|
|
378
|
+
* .floatParameter("Speed")
|
|
379
|
+
* .state("Idle", { clip: idleClip, loop: true })
|
|
380
|
+
* .state("Walk", { clip: walkClip, loop: true })
|
|
381
|
+
* .transition("Idle", "Walk", { duration: 0.25 })
|
|
382
|
+
* .condition("Speed", "greater", 0.1)
|
|
383
|
+
* .transition("Walk", "Idle", { duration: 0.25 })
|
|
384
|
+
* .condition("Speed", "less", 0.1)
|
|
385
|
+
* .build();
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
static build(name?: string): AnimatorControllerBuilder {
|
|
389
|
+
return new AnimatorControllerBuilder(name);
|
|
390
|
+
}
|
|
391
|
+
|
|
122
392
|
/**
|
|
123
393
|
* Plays an animation state by name or hash.
|
|
124
394
|
*
|