@needle-tools/engine 5.0.3 → 5.1.0-alpha.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +31 -4
  2. package/README.md +6 -7
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-BXk8jYW3.js → needle-engine.bundle-BGyKqxBH.js} +12394 -11786
  5. package/dist/needle-engine.bundle-CiYtOO2O.min.js +1732 -0
  6. package/dist/needle-engine.bundle-DzVx9Z8D.umd.cjs +1732 -0
  7. package/dist/needle-engine.d.ts +660 -63
  8. package/dist/needle-engine.js +579 -566
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/dist/{vendor-vHLk8sXu.js → vendor-CAcsI0eU.js} +116 -115
  12. package/dist/{vendor-CntUvmJu.umd.cjs → vendor-CEM38hLE.umd.cjs} +2 -2
  13. package/dist/{vendor-DPbfJJ4d.min.js → vendor-HRlxIBga.min.js} +2 -2
  14. package/lib/engine/api.d.ts +2 -0
  15. package/lib/engine/api.js +2 -0
  16. package/lib/engine/api.js.map +1 -1
  17. package/lib/engine/engine_addressables.js +5 -1
  18. package/lib/engine/engine_addressables.js.map +1 -1
  19. package/lib/engine/engine_animation.d.ts +14 -7
  20. package/lib/engine/engine_animation.js +49 -9
  21. package/lib/engine/engine_animation.js.map +1 -1
  22. package/lib/engine/engine_components.js +33 -4
  23. package/lib/engine/engine_components.js.map +1 -1
  24. package/lib/engine/engine_context.d.ts +7 -2
  25. package/lib/engine/engine_context.js +10 -2
  26. package/lib/engine/engine_context.js.map +1 -1
  27. package/lib/engine/engine_gameobject.d.ts +4 -0
  28. package/lib/engine/engine_gameobject.js.map +1 -1
  29. package/lib/engine/engine_init.js +4 -0
  30. package/lib/engine/engine_init.js.map +1 -1
  31. package/lib/engine/engine_input.js +4 -1
  32. package/lib/engine/engine_input.js.map +1 -1
  33. package/lib/engine/engine_materialpropertyblock.js +0 -19
  34. package/lib/engine/engine_materialpropertyblock.js.map +1 -1
  35. package/lib/engine/engine_networking.d.ts +11 -8
  36. package/lib/engine/engine_networking.js +43 -26
  37. package/lib/engine/engine_networking.js.map +1 -1
  38. package/lib/engine/engine_networking_instantiate.d.ts +100 -5
  39. package/lib/engine/engine_networking_instantiate.js +150 -16
  40. package/lib/engine/engine_networking_instantiate.js.map +1 -1
  41. package/lib/engine/engine_networking_prefabs.d.ts +59 -0
  42. package/lib/engine/engine_networking_prefabs.js +67 -0
  43. package/lib/engine/engine_networking_prefabs.js.map +1 -0
  44. package/lib/engine/engine_physics_rapier.d.ts +3 -0
  45. package/lib/engine/engine_physics_rapier.js +13 -9
  46. package/lib/engine/engine_physics_rapier.js.map +1 -1
  47. package/lib/engine/engine_utils.js +2 -2
  48. package/lib/engine/engine_utils.js.map +1 -1
  49. package/lib/engine/postprocessing/api.d.ts +2 -0
  50. package/lib/engine/postprocessing/api.js +2 -0
  51. package/lib/engine/postprocessing/api.js.map +1 -0
  52. package/lib/engine/postprocessing/index.d.ts +2 -0
  53. package/lib/engine/postprocessing/index.js +2 -0
  54. package/lib/engine/postprocessing/index.js.map +1 -0
  55. package/lib/engine/postprocessing/postprocessing.d.ts +83 -0
  56. package/lib/engine/postprocessing/postprocessing.js +280 -0
  57. package/lib/engine/postprocessing/postprocessing.js.map +1 -0
  58. package/lib/engine/postprocessing/types.d.ts +39 -0
  59. package/lib/engine/postprocessing/types.js +2 -0
  60. package/lib/engine/postprocessing/types.js.map +1 -0
  61. package/lib/engine/webcomponents/WebXRButtons.js +17 -3
  62. package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
  63. package/lib/engine/xr/NeedleXRSession.d.ts +2 -0
  64. package/lib/engine/xr/NeedleXRSession.js +47 -14
  65. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  66. package/lib/engine/xr/events.d.ts +30 -3
  67. package/lib/engine/xr/events.js +38 -0
  68. package/lib/engine/xr/events.js.map +1 -1
  69. package/lib/engine/xr/init.d.ts +4 -0
  70. package/lib/engine/xr/init.js +43 -0
  71. package/lib/engine/xr/init.js.map +1 -0
  72. package/lib/engine-components/AnimationUtils.d.ts +4 -1
  73. package/lib/engine-components/AnimationUtils.js +7 -19
  74. package/lib/engine-components/AnimationUtils.js.map +1 -1
  75. package/lib/engine-components/AnimatorController.d.ts +135 -2
  76. package/lib/engine-components/AnimatorController.js +216 -13
  77. package/lib/engine-components/AnimatorController.js.map +1 -1
  78. package/lib/engine-components/SeeThrough.d.ts +0 -2
  79. package/lib/engine-components/SeeThrough.js +0 -89
  80. package/lib/engine-components/SeeThrough.js.map +1 -1
  81. package/lib/engine-components/SyncedRoom.d.ts +4 -0
  82. package/lib/engine-components/SyncedRoom.js +23 -8
  83. package/lib/engine-components/SyncedRoom.js.map +1 -1
  84. package/lib/engine-components/SyncedTransform.js +5 -5
  85. package/lib/engine-components/SyncedTransform.js.map +1 -1
  86. package/lib/engine-components/Voip.d.ts +46 -0
  87. package/lib/engine-components/Voip.js +126 -2
  88. package/lib/engine-components/Voip.js.map +1 -1
  89. package/lib/engine-components/api.d.ts +1 -0
  90. package/lib/engine-components/api.js +1 -0
  91. package/lib/engine-components/api.js.map +1 -1
  92. package/lib/engine-components/codegen/components.d.ts +1 -0
  93. package/lib/engine-components/codegen/components.js +1 -0
  94. package/lib/engine-components/codegen/components.js.map +1 -1
  95. package/lib/engine-components/postprocessing/Effects/Tonemapping.d.ts +5 -2
  96. package/lib/engine-components/postprocessing/Effects/Tonemapping.js +11 -18
  97. package/lib/engine-components/postprocessing/Effects/Tonemapping.js.map +1 -1
  98. package/lib/engine-components/postprocessing/PostProcessingEffect.d.ts +3 -4
  99. package/lib/engine-components/postprocessing/PostProcessingEffect.js +6 -15
  100. package/lib/engine-components/postprocessing/PostProcessingEffect.js.map +1 -1
  101. package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +2 -1
  102. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  103. package/lib/engine-components/postprocessing/Volume.d.ts +18 -11
  104. package/lib/engine-components/postprocessing/Volume.js +61 -140
  105. package/lib/engine-components/postprocessing/Volume.js.map +1 -1
  106. package/lib/engine-components/postprocessing/index.d.ts +1 -0
  107. package/lib/engine-components/postprocessing/index.js +1 -0
  108. package/lib/engine-components/postprocessing/index.js.map +1 -1
  109. package/lib/engine-components/postprocessing/utils.d.ts +2 -0
  110. package/lib/engine-components/postprocessing/utils.js +2 -0
  111. package/lib/engine-components/postprocessing/utils.js.map +1 -1
  112. package/lib/engine-components/ui/Canvas.js +2 -2
  113. package/lib/engine-components/ui/Canvas.js.map +1 -1
  114. package/lib/engine-components/ui/Graphic.d.ts +3 -3
  115. package/lib/engine-components/ui/Graphic.js +6 -2
  116. package/lib/engine-components/ui/Graphic.js.map +1 -1
  117. package/lib/engine-components/ui/Text.d.ts +64 -11
  118. package/lib/engine-components/ui/Text.js +154 -45
  119. package/lib/engine-components/ui/Text.js.map +1 -1
  120. package/lib/engine-components/ui/index.d.ts +1 -0
  121. package/lib/engine-components/ui/index.js +1 -0
  122. package/lib/engine-components/ui/index.js.map +1 -1
  123. package/lib/engine-components-experimental/networking/PlayerSync.d.ts +25 -3
  124. package/lib/engine-components-experimental/networking/PlayerSync.js +60 -11
  125. package/lib/engine-components-experimental/networking/PlayerSync.js.map +1 -1
  126. package/package.json +5 -4
  127. package/plugins/common/logger.js +42 -19
  128. package/plugins/vite/ai.d.ts +11 -10
  129. package/plugins/vite/ai.js +305 -31
  130. package/plugins/vite/logger.client.js +4 -3
  131. package/src/engine/api.ts +3 -0
  132. package/src/engine/engine_addressables.ts +4 -1
  133. package/src/engine/engine_animation.ts +47 -9
  134. package/src/engine/engine_components.ts +36 -7
  135. package/src/engine/engine_context.ts +11 -2
  136. package/src/engine/engine_gameobject.ts +5 -0
  137. package/src/engine/engine_init.ts +4 -0
  138. package/src/engine/engine_input.ts +2 -1
  139. package/src/engine/engine_materialpropertyblock.ts +0 -19
  140. package/src/engine/engine_networking.ts +46 -23
  141. package/src/engine/engine_networking_instantiate.ts +160 -18
  142. package/src/engine/engine_networking_prefabs.ts +80 -0
  143. package/src/engine/engine_physics_rapier.ts +14 -9
  144. package/src/engine/engine_utils.ts +2 -2
  145. package/src/engine/postprocessing/api.ts +2 -0
  146. package/src/engine/postprocessing/index.ts +2 -0
  147. package/src/engine/postprocessing/postprocessing.ts +322 -0
  148. package/src/engine/postprocessing/types.ts +43 -0
  149. package/src/engine/webcomponents/WebXRButtons.ts +21 -4
  150. package/src/engine/xr/NeedleXRSession.ts +55 -20
  151. package/src/engine/xr/events.ts +44 -1
  152. package/src/engine/xr/init.ts +49 -0
  153. package/src/engine-components/AnimationUtils.ts +7 -17
  154. package/src/engine-components/AnimatorController.ts +288 -18
  155. package/src/engine-components/SeeThrough.ts +0 -116
  156. package/src/engine-components/SyncedRoom.ts +28 -9
  157. package/src/engine-components/SyncedTransform.ts +5 -5
  158. package/src/engine-components/Voip.ts +129 -2
  159. package/src/engine-components/api.ts +1 -0
  160. package/src/engine-components/codegen/components.ts +1 -0
  161. package/src/engine-components/postprocessing/Effects/Tonemapping.ts +16 -24
  162. package/src/engine-components/postprocessing/PostProcessingEffect.ts +9 -16
  163. package/src/engine-components/postprocessing/PostProcessingHandler.ts +2 -1
  164. package/src/engine-components/postprocessing/Volume.ts +72 -163
  165. package/src/engine-components/postprocessing/index.ts +1 -0
  166. package/src/engine-components/postprocessing/utils.ts +2 -0
  167. package/src/engine-components/ui/Canvas.ts +2 -2
  168. package/src/engine-components/ui/Graphic.ts +7 -3
  169. package/src/engine-components/ui/Text.ts +170 -52
  170. package/src/engine-components/ui/index.ts +2 -1
  171. package/src/engine-components-experimental/networking/PlayerSync.ts +64 -11
  172. package/dist/needle-engine.bundle-CNH61kLA.umd.cjs +0 -1730
  173. package/dist/needle-engine.bundle-Dvh1jROn.min.js +0 -1730
@@ -0,0 +1,49 @@
1
+ import { DeviceUtilities, setParamWithoutReload } from "../engine_utils.js";
2
+ import { NeedleXRSession } from "./NeedleXRSession.js";
3
+
4
+ /**
5
+ * Initialize XR subsystem. Called from `initEngine()`
6
+ */
7
+ export function initXR() {
8
+ // Prewarm AR support check
9
+ NeedleXRSession.isARSupported();
10
+
11
+
12
+
13
+ if (DeviceUtilities.isiOS()) {
14
+
15
+ // Prefetch
16
+ const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
17
+ url.searchParams.set("url", location.href);
18
+ const urlStr = url.toString();
19
+ fetch(urlStr, { method: "HEAD", mode: "no-cors" }).catch(() => {
20
+ // appclip prefetch - to get metadata faster on iOS devices, this seems to fix the double tap issue when opening the appclip for AR sessions.
21
+ });
22
+
23
+ try {
24
+ // We add the meta tag here to preload app clip card data for iOS.
25
+ const meta = window.top?.document.querySelector('meta[name="apple-itunes-app"]');
26
+ if(!meta) {
27
+ const metaTag = document.createElement("meta");
28
+ metaTag.name = "apple-itunes-app";
29
+ metaTag.content = "app-id=6757205152, app-clip-bundle-id=tools.needle.launch-app.Clip, app-clip-display=card";
30
+ window.top?.document.head.appendChild(metaTag);
31
+ }
32
+ }
33
+ catch (e) {
34
+ console.warn("Error adding apple-itunes-app meta tag for appclip support\n", e);
35
+ }
36
+
37
+ try {
38
+ // 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?
39
+ const topWindow = window.top || window;
40
+ const preconnectMeta = topWindow.document.createElement("link");
41
+ preconnectMeta.rel = "preconnect";
42
+ preconnectMeta.href = url.toString();
43
+ topWindow.document.head.appendChild(preconnectMeta);
44
+ }
45
+ catch (e) {
46
+ console.warn("Error adding preconnect link for appclip.apple.com\n", e);
47
+ }
48
+ }
49
+ }
@@ -1,29 +1,19 @@
1
1
  import { Object3D } from "three";
2
2
 
3
- const $objectAnimationKey = Symbol("objectIsAnimatedData");
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
- if (!obj) return;
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
- if (!obj) return false;
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 the static method {@link AnimatorController.createFromClips} to create
54
- * an animator controller from a set of animation clips.
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
  *
@@ -176,26 +176,6 @@ export class SeeThrough extends Behaviour {
176
176
  onEnable() {
177
177
  this._needsUpdate = true;
178
178
  this._renderer = null;
179
- // SeeThroughUsdzExporterPlugin.components.push(this);
180
- }
181
-
182
- /** @internal */
183
- onDisable() {
184
- // this._renderer?.forEach(r => {
185
- // const original = this.rendererMaterialsOriginal.get(r);
186
- // for (let i = 0; i < r.sharedMaterials.length; i++) {
187
- // const mat = r.sharedMaterials[i];
188
- // if (!mat) continue;
189
- // if (original && original[i]) {
190
- // r.sharedMaterials[i] = original[i];
191
- // }
192
- // }
193
- // this.rendererMaterials.delete(r);
194
- // this.rendererMaterialsOriginal.delete(r);
195
- // });
196
-
197
- // const index = SeeThroughUsdzExporterPlugin.components.indexOf(this);
198
- // if (index !== -1) SeeThroughUsdzExporterPlugin.components.splice(index, 1);
199
179
  }
200
180
 
201
181
  /**
@@ -207,7 +187,6 @@ export class SeeThrough extends Behaviour {
207
187
  if (this._needsUpdate) {
208
188
  this._needsUpdate = false;
209
189
  this._renderer = this.gameObject.getComponentsInChildren(Renderer);
210
-
211
190
  // NOTE: instead of using the object's anchor (gameObject.worldPosition) we could also get the object's bounding box center:
212
191
  // getBoundingBox(this.gameObject); // < import { getBoundingBox } from "@needle-tools/engine";
213
192
  this.updateDirection();
@@ -216,12 +195,9 @@ export class SeeThrough extends Behaviour {
216
195
  this.updateDirection();
217
196
  }
218
197
 
219
-
220
-
221
198
  if (!this.autoUpdate) return;
222
199
  if (!this.referencePoint) return;
223
200
 
224
-
225
201
  const dot = this._referencePointDir.dot(this.context.mainCamera.worldForward);
226
202
  const shouldHide = dot > .2;
227
203
 
@@ -272,38 +248,10 @@ export class SeeThrough extends Behaviour {
272
248
  renderer.gameObject.raycastAllowed = true;
273
249
  }
274
250
 
275
- // if (!this.rendererMaterials.has(renderer)) {
276
- // const originalMaterials = new Array<Material>();
277
- // const clonedMaterials = new Array<MaterialWithState>();
278
-
279
- // // We clone the materials once and store them, so we can modify the opacity without affecting other objects using the same material. This could potentially be optimized further to re-use materials between renderers if multiple renderers use the same material.
280
- // for (let i = 0; i < renderer.sharedMaterials.length; i++) {
281
- // const mat = renderer.sharedMaterials[i];
282
- // if (!mat) continue;
283
- // originalMaterials.push(mat);
284
- // const matClone = mat.clone() as MaterialWithState;
285
- // // @ts-ignore
286
- // matClone.userData = mat.userData || {};
287
- // matClone.userData.seeThrough = {
288
- // initial: {
289
- // opacity: matClone.opacity,
290
- // transparent: matClone.transparent,
291
- // alphaHash: matClone.alphaHash
292
- // }
293
- // }
294
- // clonedMaterials.push(matClone);
295
- // // renderer.sharedMaterials[i] = matClone;
296
- // }
297
-
298
- // this.rendererMaterials.set(renderer, clonedMaterials);
299
- // this.rendererMaterialsOriginal.set(renderer, originalMaterials);
300
- // }
301
-
302
251
  const materials = renderer.sharedMaterials;// : this.rendererMaterials.get(renderer);
303
252
  if (!materials) return;
304
253
 
305
254
  const block = MaterialPropertyBlock.get(renderer.gameObject);
306
-
307
255
  const currentOpacity = (block.getOverride("opacity")?.value ?? materials[0].opacity ?? 1);
308
256
 
309
257
  let newAlpha = Mathf.lerp(currentOpacity, targetAlpha, duration <= 0 ? 1 : this.context.time.deltaTime / duration);;
@@ -315,72 +263,8 @@ export class SeeThrough extends Behaviour {
315
263
  block.setOverride("alphaHash", this.useAlphaHash);
316
264
  block.setOverride("opacity", newAlpha);
317
265
  block.setOverride("transparent", newAlpha >= 0.99999 ? false : !this.useAlphaHash);
318
-
319
-
320
- // for (const mat of materials) {
321
- // if (!mat) continue;
322
-
323
- // let newAlpha = Mathf.lerp(mat.opacity, targetAlpha, duration <= 0 ? 1 : this.context.time.deltaTime / duration);;
324
- // if (newAlpha >= 0.99) newAlpha = 1;
325
- // else if (newAlpha <= 0.01) newAlpha = 0;
326
-
327
-
328
- // const wasTransparent = mat.transparent;
329
- // const wasAlphaHash = mat.alphaHash;
330
- // const previousOpacity = mat.opacity;
331
-
332
- // mat.alphaHash = this.useAlphaHash;
333
-
334
- // if (mat.userData && "seeThrough" in mat.userData) {
335
- // const initial = mat.userData.seeThrough.initial as MaterialState;
336
- // mat.opacity = initial.opacity * newAlpha;
337
- // mat.transparent = mat.opacity >= 1 ? initial.transparent : !this.useAlphaHash;
338
- // }
339
- // else {
340
- // mat.transparent = mat.opacity >= 1 ? false : !this.useAlphaHash;
341
- // }
342
-
343
- // if (wasTransparent !== mat.transparent
344
- // || wasAlphaHash !== mat.alphaHash
345
- // || mat.opacity !== previousOpacity // MeshPhysicsMaterial needs that and maybe other materials too...
346
- // ) {
347
- // mat.needsUpdate = true;
348
- // }
349
- // }
350
266
  });
351
267
  }
352
268
 
353
269
  }
354
270
 
355
-
356
- ;
357
- // class SeeThroughUsdzExporterPlugin implements IUSDExporterExtension {
358
-
359
- // static readonly components: SeeThrough[] = [];
360
-
361
- // get extensionName() {
362
- // return "SeeThrough";
363
- // }
364
-
365
- // // onExportObject(object: Object3D<Object3DEventMap>, model: USDObject, context: USDZExporterContext) {
366
- // // const component = SeeThroughUsdzExporterPlugin.components.find(c => c.gameObject === object);
367
- // // if(!component) return;
368
- // // console.log("OH MY GOD SEE THROUGH USDZ EXPORTER", component, model);
369
-
370
- // // model.materialName = "AlphaHashMaterialInstance"; // we could make this unique per object if needed
371
-
372
- // // model.addEventListener("serialize", (writer, context) => {
373
- // // writer.appendLine(`# SeeThrough component on ${object.name}`);
374
- // // });
375
- // // }
376
-
377
- // }
378
-
379
- // const seeThroughUsdzExporterPlugin = new SeeThroughUsdzExporterPlugin();
380
-
381
- // USDZExporter.beforeExport.addEventListener(args => {
382
- // if (SeeThroughUsdzExporterPlugin.components.length === 0) return;
383
- // if (args.exporter.extensions.includes(seeThroughUsdzExporterPlugin) === false) {
384
- // args.exporter.extensions.push(seeThroughUsdzExporterPlugin);
385
- // }
386
- // });
@@ -121,8 +121,36 @@ export class SyncedRoom extends Behaviour {
121
121
  if (debug) console.log(`SyncedRoom roomName:${this.roomName}, urlParamName:${this.urlParameterName}, joinRandomRoom:${this.joinRandomRoom}`);
122
122
  }
123
123
 
124
+ private _hasConnectedBefore: boolean = false;
125
+
124
126
  /** @internal */
125
127
  onEnable() {
128
+ if (this.createJoinButton) {
129
+ const button = this.createRoomButton();
130
+ this.context.menu.appendChild(button);
131
+ }
132
+ if (this.createViewOnlyButton) {
133
+ this.onEnableViewOnlyButton()
134
+ }
135
+
136
+ // On re-enable (after disable), reconnect immediately
137
+ if (this._hasConnectedBefore) {
138
+ this._connectToRoom();
139
+ }
140
+ }
141
+
142
+ /** @internal */
143
+ start() {
144
+ // Defer initial connection to start() so other components added in the same frame
145
+ // have time to register their listeners in awake/onEnable (e.g. PlayerSync)
146
+ if (!this._hasConnectedBefore) {
147
+ this._connectToRoom();
148
+ }
149
+ }
150
+
151
+ private _connectToRoom() {
152
+ this._hasConnectedBefore = true;
153
+
126
154
  // if the url contains a view parameter override room and join in view mode
127
155
  const viewId = utils.getParam(viewParamName);
128
156
  if (viewId && typeof viewId === "string" && viewId.length > 0) {
@@ -130,16 +158,7 @@ export class SyncedRoom extends Behaviour {
130
158
  this.context.connection.joinRoom(viewId, true);
131
159
  return;
132
160
  }
133
- // If setup to join a random room
134
161
  this.tryJoinRoom();
135
-
136
- if (this.createJoinButton) {
137
- const button = this.createRoomButton();
138
- this.context.menu.appendChild(button);
139
- }
140
- if (this.createViewOnlyButton) {
141
- this.onEnableViewOnlyButton()
142
- }
143
162
  }
144
163
 
145
164
  /** @internal */
@@ -145,7 +145,7 @@ export class SyncedTransform extends Behaviour {
145
145
  */
146
146
  public requestOwnership() {
147
147
  if (debug)
148
- console.log("Request ownership");
148
+ console.log("[SyncedTransform] Request ownership");
149
149
  if (!this._model) {
150
150
  this._shouldRequestOwnership = true;
151
151
  this._needsUpdate = true;
@@ -189,7 +189,7 @@ export class SyncedTransform extends Behaviour {
189
189
  /** @internal */
190
190
  awake() {
191
191
  if (debug)
192
- console.log("new instance", this.guid, this);
192
+ console.log("[SyncedTransform] new instance", this.guid, this);
193
193
  this._receivedDataBefore = false;
194
194
  this._targetPosition = new Vector3();
195
195
  this._targetRotation = new Quaternion();
@@ -245,7 +245,7 @@ export class SyncedTransform extends Behaviour {
245
245
  if (this.destroyed) return;
246
246
  if (typeof data.guid === "function" && data.guid() === this.guid) {
247
247
  if (debug)
248
- console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
248
+ console.log("[SyncedTransform] new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
249
249
  this.receivedUpdate = true;
250
250
  this._receivedFastUpdate = data.fast();
251
251
  const transform = data.transform();
@@ -323,7 +323,7 @@ export class SyncedTransform extends Behaviour {
323
323
 
324
324
  if (!this.context.connection.isInRoom || !this._model) {
325
325
  if (debug)
326
- console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
326
+ console.log("[SyncedTransform] no model or room", this.name, this.guid, this.context.connection.isInRoom);
327
327
  return;
328
328
  }
329
329
 
@@ -409,7 +409,7 @@ export class SyncedTransform extends Behaviour {
409
409
  if (this.rb && this.overridePhysics) {
410
410
  if (this._wasKinematic !== undefined) {
411
411
  if (debug)
412
- console.log("reset kinematic", this.rb.name, this._wasKinematic);
412
+ console.log("[SyncedTransform] reset kinematic", this.rb.name, this._wasKinematic);
413
413
  this.rb.isKinematic = this._wasKinematic;
414
414
  }
415
415