@needle-tools/engine 5.1.0-alpha.4 → 5.1.0-alpha.5

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 (96) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-B7cqsI4c.js → needle-engine.bundle-C-LG00ZZ.js} +6570 -6271
  4. package/dist/{needle-engine.bundle-AjVIot3d.min.js → needle-engine.bundle-D7tzaiYE.min.js} +157 -157
  5. package/dist/{needle-engine.bundle-DQCuBTVp.umd.cjs → needle-engine.bundle-OPkPmdUM.umd.cjs} +140 -140
  6. package/dist/needle-engine.d.ts +668 -191
  7. package/dist/needle-engine.js +597 -595
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/dist/three.js +1 -0
  11. package/dist/three.min.js +21 -21
  12. package/dist/three.umd.cjs +16 -16
  13. package/lib/engine/api.d.ts +2 -0
  14. package/lib/engine/api.js +2 -0
  15. package/lib/engine/api.js.map +1 -1
  16. package/lib/engine/codegen/register_types.js +10 -10
  17. package/lib/engine/codegen/register_types.js.map +1 -1
  18. package/lib/engine/engine_camera.fit.js +16 -4
  19. package/lib/engine/engine_camera.fit.js.map +1 -1
  20. package/lib/engine/engine_context.d.ts +20 -7
  21. package/lib/engine/engine_context.js +29 -14
  22. package/lib/engine/engine_context.js.map +1 -1
  23. package/lib/engine/engine_context_eventbus.d.ts +47 -0
  24. package/lib/engine/engine_context_eventbus.js +47 -0
  25. package/lib/engine/engine_context_eventbus.js.map +1 -0
  26. package/lib/engine/engine_input.d.ts +23 -4
  27. package/lib/engine/engine_input.js +2 -1
  28. package/lib/engine/engine_input.js.map +1 -1
  29. package/lib/engine/engine_physics_rapier.d.ts +10 -0
  30. package/lib/engine/engine_physics_rapier.js +6 -0
  31. package/lib/engine/engine_physics_rapier.js.map +1 -1
  32. package/lib/engine/engine_types.d.ts +10 -0
  33. package/lib/engine-components/AnimationBuilder.d.ts +158 -0
  34. package/lib/engine-components/AnimationBuilder.js +305 -0
  35. package/lib/engine-components/AnimationBuilder.js.map +1 -0
  36. package/lib/engine-components/Animator.js +6 -1
  37. package/lib/engine-components/Animator.js.map +1 -1
  38. package/lib/engine-components/AnimatorController.builder.d.ts +101 -23
  39. package/lib/engine-components/AnimatorController.builder.js +88 -20
  40. package/lib/engine-components/AnimatorController.builder.js.map +1 -1
  41. package/lib/engine-components/AnimatorController.js +2 -0
  42. package/lib/engine-components/AnimatorController.js.map +1 -1
  43. package/lib/engine-components/ContactShadows.d.ts +1 -0
  44. package/lib/engine-components/ContactShadows.js +14 -1
  45. package/lib/engine-components/ContactShadows.js.map +1 -1
  46. package/lib/engine-components/DropListener.js +3 -0
  47. package/lib/engine-components/DropListener.js.map +1 -1
  48. package/lib/engine-components/OrbitControls.d.ts +0 -2
  49. package/lib/engine-components/OrbitControls.js +14 -1
  50. package/lib/engine-components/OrbitControls.js.map +1 -1
  51. package/lib/engine-components/SceneSwitcher.js +3 -0
  52. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  53. package/lib/engine-components/api.d.ts +1 -0
  54. package/lib/engine-components/api.js +1 -0
  55. package/lib/engine-components/api.js.map +1 -1
  56. package/lib/engine-components/codegen/components.d.ts +6 -6
  57. package/lib/engine-components/codegen/components.js +6 -6
  58. package/lib/engine-components/codegen/components.js.map +1 -1
  59. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  60. package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -7
  61. package/lib/engine-components/timeline/PlayableDirector.js +6 -6
  62. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  63. package/lib/engine-components/timeline/TimelineBuilder.d.ts +175 -9
  64. package/lib/engine-components/timeline/TimelineBuilder.js +108 -2
  65. package/lib/engine-components/timeline/TimelineBuilder.js.map +1 -1
  66. package/lib/engine-components/timeline/TimelineTracks.d.ts +15 -7
  67. package/lib/engine-components/timeline/TimelineTracks.js +22 -14
  68. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  69. package/lib/engine-components/web/CursorFollow.d.ts +0 -1
  70. package/lib/engine-components/web/CursorFollow.js +0 -1
  71. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  72. package/package.json +1 -1
  73. package/plugins/common/cloud.js +6 -1
  74. package/plugins/vite/license.js +19 -1
  75. package/src/engine/api.ts +3 -0
  76. package/src/engine/codegen/register_types.ts +10 -10
  77. package/src/engine/engine_camera.fit.ts +15 -4
  78. package/src/engine/engine_context.ts +30 -15
  79. package/src/engine/engine_context_eventbus.ts +73 -0
  80. package/src/engine/engine_input.ts +27 -6
  81. package/src/engine/engine_physics_rapier.ts +20 -6
  82. package/src/engine/engine_types.ts +22 -12
  83. package/src/engine-components/AnimationBuilder.ts +472 -0
  84. package/src/engine-components/Animator.ts +6 -1
  85. package/src/engine-components/AnimatorController.builder.ts +163 -37
  86. package/src/engine-components/AnimatorController.ts +1 -0
  87. package/src/engine-components/ContactShadows.ts +15 -1
  88. package/src/engine-components/DropListener.ts +3 -0
  89. package/src/engine-components/OrbitControls.ts +16 -5
  90. package/src/engine-components/SceneSwitcher.ts +3 -0
  91. package/src/engine-components/api.ts +1 -0
  92. package/src/engine-components/codegen/components.ts +6 -6
  93. package/src/engine-components/timeline/PlayableDirector.ts +20 -20
  94. package/src/engine-components/timeline/TimelineBuilder.ts +277 -17
  95. package/src/engine-components/timeline/TimelineTracks.ts +24 -16
  96. package/src/engine-components/web/CursorFollow.ts +0 -1
@@ -1,17 +1,34 @@
1
- import { AnimationClip } from "three";
1
+ import { AnimationClip, Object3D } from "three";
2
+ import type { Light, Material, PerspectiveCamera } from "three";
2
3
 
3
4
  import { AnimatorConditionMode, AnimatorControllerParameterType } from "../engine/extensions/NEEDLE_animator_controller_model.js";
4
5
  import type { AnimatorControllerModel, Condition, Parameter, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
5
6
  import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
6
7
  import { AnimatorController } from "./AnimatorController.js";
8
+ import { resolveClipSource, track as trackFn, type TrackDescriptor, type TrackOptions, type AnimationKeyframe, type Tween, type Vec3Value, type QuatValue, type EulerValue, type ColorValue } from "./AnimationBuilder.js";
9
+
10
+ /** Keyframe array or tween shorthand */
11
+ type KF<V> = AnimationKeyframe<V>[] | Tween<V>;
12
+
13
+ /** Extracts parameter names of a given type from the builder's tracked parameter map */
14
+ type ParamNamesOfType<TParams, PType extends string> = {
15
+ [K in keyof TParams & string]: TParams[K] extends PType ? K : never
16
+ }[keyof TParams & string];
7
17
 
8
18
 
9
19
  /**
10
20
  * Configuration for an animation state in the builder
11
21
  */
12
22
  export declare type StateOptions = {
13
- /** The animation clip for this state */
14
- clip: AnimationClip;
23
+ /**
24
+ * The animation clip for this state. Accepts:
25
+ * - A pre-built `AnimationClip`
26
+ * - A single {@link TrackDescriptor} from {@link track}
27
+ * - An array of {@link TrackDescriptor}s (multiple tracks combined into one clip)
28
+ *
29
+ * When omitted, use {@link AnimatorControllerBuilder.track .track()} to define animation tracks inline.
30
+ */
31
+ clip?: AnimationClip | TrackDescriptor | TrackDescriptor[];
15
32
  /** Whether the animation should loop (default: false) */
16
33
  loop?: boolean;
17
34
  /** Base speed multiplier (default: 1) */
@@ -30,10 +47,8 @@ export declare type StateOptions = {
30
47
  export declare type TransitionOptions = {
31
48
  /** Duration of the crossfade in seconds (default: 0) */
32
49
  duration?: number;
33
- /** Normalized exit time 0-1 (default: 1). Only used when hasExitTime is true */
50
+ /** Normalized exit time 0-1. When set, the transition waits until the source animation reaches this point before transitioning. */
34
51
  exitTime?: number;
35
- /** Whether the transition waits for exitTime before transitioning (default: false) */
36
- hasExitTime?: boolean;
37
52
  /** Normalized offset into the destination state's animation (default: 0) */
38
53
  offset?: number;
39
54
  /** Whether duration is in seconds (true) or normalized (false) (default: false) */
@@ -63,17 +78,22 @@ type BuilderTransition = {
63
78
  type BuilderState = {
64
79
  name: string;
65
80
  options: StateOptions;
81
+ inlineTracks: TrackDescriptor[];
66
82
  transitions: BuilderTransition[];
67
83
  };
68
84
 
69
85
  /**
70
86
  * A fluent builder for creating {@link AnimatorController} instances from code.
71
87
  *
72
- * Use {@link AnimatorController.build} to create a new builder.
88
+ * Use {@link AnimatorControllerBuilder.create} or {@link AnimatorController.build} to create a new builder.
73
89
  *
74
- * @example
90
+ * The builder tracks state names and parameter types through the fluent chain,
91
+ * providing autocomplete for state names in `.transition()` and type-aware
92
+ * `.condition()` calls (e.g., trigger parameters don't require a mode argument).
93
+ *
94
+ * @example With pre-built AnimationClips
75
95
  * ```ts
76
- * const controller = AnimatorController.build("CharacterController")
96
+ * const controller = AnimatorControllerBuilder.create("CharacterController")
77
97
  * .floatParameter("Speed", 0)
78
98
  * .triggerParameter("Jump")
79
99
  * .state("Idle", { clip: idleClip, loop: true })
@@ -84,58 +104,100 @@ type BuilderState = {
84
104
  * .transition("Walk", "Idle", { duration: 0.25 })
85
105
  * .condition("Speed", "less", 0.1)
86
106
  * .transition("*", "Jump", { duration: 0.1 })
87
- * .condition("Jump", "if")
107
+ * .condition("Jump")
88
108
  * .transition("Jump", "Idle", { hasExitTime: true, exitTime: 0.9, duration: 0.25 })
89
109
  * .build();
90
110
  * ```
91
111
  *
112
+ * @example With inline tracks (no pre-built clips needed)
113
+ * ```ts
114
+ * const controller = AnimatorControllerBuilder.create("Door")
115
+ * .boolParameter("Open", false)
116
+ * .state("Closed", { loop: true })
117
+ * .track(door, "position", { from: [0, 0, 0], to: [0, 0, 0], duration: 1 })
118
+ * .state("Open", { loop: true })
119
+ * .track(door, "position", { from: [0, 0, 0], to: [2, 0, 0], duration: 1 })
120
+ * .track(light, "intensity", { from: 0, to: 5, duration: 1 })
121
+ * .transition("Closed", "Open", { duration: 0.25 })
122
+ * .condition("Open", "if")
123
+ * .transition("Open", "Closed", { duration: 0.25 })
124
+ * .condition("Open", "ifNot")
125
+ * .build(room);
126
+ * ```
127
+ *
128
+ * @typeParam TStates - Union of state names added via `.state()`. Used for autocomplete and validation in `.transition()` and `.defaultState()`.
129
+ * @typeParam TParams - Record mapping parameter names to their types (`"trigger"`, `"bool"`, `"float"`, `"int"`). Used for type-aware `.condition()` overloads.
130
+ *
92
131
  * @category Animation and Sequencing
93
132
  * @group Utilities
94
133
  */
95
- export class AnimatorControllerBuilder {
134
+ export class AnimatorControllerBuilder<
135
+ TStates extends string = never,
136
+ TParams extends Record<string, "trigger" | "bool" | "float" | "int"> = {},
137
+ > {
96
138
  private _name: string;
97
139
  private _parameters: Parameter[] = [];
98
140
  private _states: BuilderState[] = [];
99
141
  private _anyStateTransitions: BuilderTransition[] = [];
100
142
  private _defaultStateName: string | null = null;
101
143
  private _lastTransition: BuilderTransition | null = null;
144
+ private _lastState: BuilderState | null = null;
145
+
146
+ /**
147
+ * Creates a new AnimatorControllerBuilder instance.
148
+ * @param name - Optional name for the controller
149
+ */
150
+ static create(name?: string): AnimatorControllerBuilder {
151
+ return new AnimatorControllerBuilder(name);
152
+ }
102
153
 
103
154
  constructor(name?: string) {
104
155
  this._name = name ?? "AnimatorController";
105
156
  }
106
157
 
107
158
  /** Adds a float parameter */
108
- floatParameter(name: string, defaultValue: number = 0): this {
159
+ floatParameter<N extends string>(name: N, defaultValue: number = 0): AnimatorControllerBuilder<TStates, TParams & Record<N, "float">> {
109
160
  this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Float, value: defaultValue });
110
- return this;
161
+ return this as any;
111
162
  }
112
163
 
113
164
  /** Adds an integer parameter */
114
- intParameter(name: string, defaultValue: number = 0): this {
165
+ intParameter<N extends string>(name: N, defaultValue: number = 0): AnimatorControllerBuilder<TStates, TParams & Record<N, "int">> {
115
166
  this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Int, value: defaultValue });
116
- return this;
167
+ return this as any;
117
168
  }
118
169
 
119
170
  /** Adds a boolean parameter */
120
- boolParameter(name: string, defaultValue: boolean = false): this {
171
+ boolParameter<N extends string>(name: N, defaultValue: boolean = false): AnimatorControllerBuilder<TStates, TParams & Record<N, "bool">> {
121
172
  this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Bool, value: defaultValue });
122
- return this;
173
+ return this as any;
123
174
  }
124
175
 
125
176
  /** Adds a trigger parameter */
126
- triggerParameter(name: string): this {
177
+ triggerParameter<N extends string>(name: N): AnimatorControllerBuilder<TStates, TParams & Record<N, "trigger">> {
127
178
  this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Trigger, value: false });
128
- return this;
179
+ return this as any;
129
180
  }
130
181
 
131
182
  /**
132
183
  * Adds a state to the controller. The first state added becomes the default state.
184
+ *
185
+ * When `options.clip` is provided, the state uses that clip directly.
186
+ * When omitted, chain `.track()` calls to define animation tracks inline:
187
+ * ```ts
188
+ * .state("Open", { loop: true })
189
+ * .track(door, "position", { from: [0,0,0], to: [2,0,0], duration: 1 })
190
+ * .track(light, "intensity", { from: 0, to: 5, duration: 1 })
191
+ * ```
192
+ *
133
193
  * @param name - Unique name for the state
134
- * @param options - State configuration including clip, loop, speed
194
+ * @param options - State configuration including clip, loop, speed. When omitted, use `.track()` to add animation data.
135
195
  */
136
- state(name: string, options: StateOptions): this {
137
- this._states.push({ name, options, transitions: [] });
138
- return this;
196
+ state<N extends string>(name: N, options?: StateOptions): AnimatorControllerBuilder<TStates | N, TParams> {
197
+ const state: BuilderState = { name, options: options ?? {}, inlineTracks: [], transitions: [] };
198
+ this._states.push(state);
199
+ this._lastState = state;
200
+ return this as any;
139
201
  }
140
202
 
141
203
  /**
@@ -146,8 +208,9 @@ export class AnimatorControllerBuilder {
146
208
  * @param to - Destination state name
147
209
  * @param options - Transition configuration
148
210
  */
149
- transition(from: string, to: string, options?: TransitionOptions): this {
150
- const t: BuilderTransition = { to, options: options ?? {}, conditions: [] };
211
+ transition(from: TStates | "*", to: TStates, options?: TransitionOptions): AnimatorControllerBuilder<TStates, TParams> {
212
+ this._lastState = null;
213
+ const t: BuilderTransition = { to: to as string, options: options ?? {}, conditions: [] };
151
214
  if (from === "*") {
152
215
  this._anyStateTransitions.push(t);
153
216
  }
@@ -157,19 +220,68 @@ export class AnimatorControllerBuilder {
157
220
  state.transitions.push(t);
158
221
  }
159
222
  this._lastTransition = t;
160
- return this;
223
+ return this as any;
161
224
  }
162
225
 
163
226
  /**
164
227
  * Adds a condition to the most recently added transition.
165
228
  * Multiple conditions on the same transition are AND-ed together.
229
+ *
230
+ * The required arguments depend on the parameter type:
231
+ * - **Trigger**: `.condition("Jump")` — mode defaults to `"if"`, no threshold needed
232
+ * - **Bool**: `.condition("Open", "if")` or `.condition("Open", "ifNot")`
233
+ * - **Float/Int**: `.condition("Speed", "greater", 0.1)`
234
+ *
166
235
  * @param parameter - Name of the parameter to evaluate
167
- * @param mode - Condition mode: `"if"`, `"ifNot"`, `"greater"`, `"less"`, `"equals"`, `"notEqual"`
168
- * @param threshold - Comparison threshold for numeric conditions (default: 0)
169
236
  */
170
- condition(parameter: string, mode: ConditionMode, threshold: number = 0): this {
237
+ // Trigger parameters: mode is optional (defaults to "if")
238
+ condition(parameter: ParamNamesOfType<TParams, "trigger">, mode?: "if" | "ifNot"): AnimatorControllerBuilder<TStates, TParams>;
239
+ // Bool parameters: mode is required
240
+ condition(parameter: ParamNamesOfType<TParams, "bool">, mode: "if" | "ifNot"): AnimatorControllerBuilder<TStates, TParams>;
241
+ // Float/Int parameters: mode and optional threshold
242
+ condition(parameter: ParamNamesOfType<TParams, "float" | "int">, mode: "greater" | "less" | "equals" | "notEqual", threshold?: number): AnimatorControllerBuilder<TStates, TParams>;
243
+ condition(parameter: string, mode?: ConditionMode, threshold?: number): AnimatorControllerBuilder<TStates, TParams> {
171
244
  if (!this._lastTransition) throw new Error("AnimatorControllerBuilder: .condition() must be called after .transition()");
172
- this._lastTransition.conditions.push({ parameter, mode, threshold });
245
+ this._lastTransition.conditions.push({ parameter, mode: mode ?? "if", threshold: threshold ?? 0 });
246
+ return this as any;
247
+ }
248
+
249
+ // --- Object3D ---
250
+ /** Adds an animation track for an Object3D's position or scale to the current state */
251
+ track(target: Object3D, property: "position" | "scale", keyframes: KF<Vec3Value>, options?: TrackOptions): this;
252
+ /** Adds an animation track for an Object3D's quaternion to the current state */
253
+ track(target: Object3D, property: "quaternion", keyframes: KF<QuatValue>, options?: TrackOptions): this;
254
+ /** Adds an animation track for an Object3D's rotation (Euler, converted to quaternion) to the current state */
255
+ track(target: Object3D, property: "rotation", keyframes: KF<EulerValue>, options?: TrackOptions): this;
256
+ /** Adds an animation track for an Object3D's visibility to the current state */
257
+ track(target: Object3D, property: "visible", keyframes: KF<boolean>, options?: TrackOptions): this;
258
+ // --- Material ---
259
+ /** Adds an animation track for a material's numeric property to the current state */
260
+ track(target: Material, property: "opacity" | "roughness" | "metalness" | "alphaTest" | "emissiveIntensity" | "envMapIntensity" | "bumpScale" | "displacementScale" | "displacementBias", keyframes: KF<number>, options?: TrackOptions): this;
261
+ /** Adds an animation track for a material's color property to the current state */
262
+ track(target: Material, property: "color" | "emissive", keyframes: KF<ColorValue>, options?: TrackOptions): this;
263
+ // --- Light ---
264
+ /** Adds an animation track for a light's numeric property to the current state */
265
+ track(target: Light, property: "intensity" | "distance" | "angle" | "penumbra" | "decay", keyframes: KF<number>, options?: TrackOptions): this;
266
+ /** Adds an animation track for a light's color to the current state */
267
+ track(target: Light, property: "color", keyframes: KF<ColorValue>, options?: TrackOptions): this;
268
+ // --- Camera ---
269
+ /** Adds an animation track for a camera's numeric property to the current state */
270
+ track(target: PerspectiveCamera, property: "fov" | "near" | "far" | "zoom", keyframes: KF<number>, options?: TrackOptions): this;
271
+ /**
272
+ * Adds an animation track to the most recently added state.
273
+ * Must be called after `.state()`. The track has the same type-safe overloads
274
+ * as the standalone {@link track} function.
275
+ *
276
+ * @param target - The object whose type determines valid properties and value types
277
+ * @param property - The property to animate
278
+ * @param keyframes - Keyframe array or {@link Tween} shorthand
279
+ * @param options - Optional {@link TrackOptions} with a `root` for named targeting
280
+ */
281
+ track(target: object, property: string, keyframes: KF<any>, options?: TrackOptions): this {
282
+ if (!this._lastState) throw new Error("AnimatorControllerBuilder: .track() must be called after .state()");
283
+ if (this._lastState.options.clip) throw new Error(`AnimatorControllerBuilder: state "${this._lastState.name}" already has a clip. Use either .track() or { clip: ... }, not both.`);
284
+ this._lastState.inlineTracks.push(trackFn(target as Object3D, property as "position", keyframes, options));
173
285
  return this;
174
286
  }
175
287
 
@@ -178,16 +290,18 @@ export class AnimatorControllerBuilder {
178
290
  * If not called, the first added state is used.
179
291
  * @param name - Name of the state
180
292
  */
181
- defaultState(name: string): this {
182
- this._defaultStateName = name;
183
- return this;
293
+ defaultState(name: TStates): AnimatorControllerBuilder<TStates, TParams> {
294
+ this._defaultStateName = name as string;
295
+ return this as any;
184
296
  }
185
297
 
186
298
  /**
187
299
  * Builds and returns the {@link AnimatorController}.
188
300
  * Resolves all state name references to indices.
301
+ * @param root - Optional root Object3D for resolving {@link TrackDescriptor} track paths.
302
+ * When provided, tracks targeting a different object use `target.name` for named resolution.
189
303
  */
190
- build(): AnimatorController {
304
+ build(root?: Object3D): AnimatorController {
191
305
  const stateIndexMap = new Map<string, number>();
192
306
  this._states.forEach((s, i) => stateIndexMap.set(s.name, i));
193
307
 
@@ -203,7 +317,7 @@ export class AnimatorControllerBuilder {
203
317
  if (destIndex === undefined) throw new Error(`AnimatorControllerBuilder: transition target "${t.to}" not found`);
204
318
  return {
205
319
  exitTime: t.options.exitTime ?? 1,
206
- hasExitTime: t.options.hasExitTime ?? false,
320
+ hasExitTime: t.options.exitTime !== undefined,
207
321
  duration: t.options.duration ?? 0,
208
322
  offset: t.options.offset ?? 0,
209
323
  hasFixedDuration: t.options.hasFixedDuration ?? false,
@@ -226,12 +340,24 @@ export class AnimatorControllerBuilder {
226
340
  transitions.push(resolveTransition(anyT));
227
341
  }
228
342
 
343
+ // Resolve clip: from options.clip, inline .track() calls, or error
344
+ let clip: AnimationClip;
345
+ if (s.options.clip) {
346
+ clip = resolveClipSource(s.options.clip, root);
347
+ }
348
+ else if (s.inlineTracks.length > 0) {
349
+ clip = resolveClipSource(s.inlineTracks, root);
350
+ }
351
+ else {
352
+ throw new Error(`AnimatorControllerBuilder: state "${s.name}" has no clip and no inline tracks. Provide { clip } or chain .track() calls.`);
353
+ }
354
+
229
355
  return {
230
356
  name: s.name,
231
357
  hash: index,
232
358
  motion: {
233
- name: s.options.clip.name,
234
- clip: s.options.clip,
359
+ name: clip.name,
360
+ clip: clip,
235
361
  isLooping: s.options.loop ?? false,
236
362
  },
237
363
  transitions,
@@ -397,6 +397,7 @@ export class AnimatorController {
397
397
  * Stops all animations and unregisters the mixer from the animation system.
398
398
  */
399
399
  dispose() {
400
+ if (!this._mixer) return;
400
401
  this._mixer.stopAllAction();
401
402
  if (this.animator) {
402
403
  this._mixer.uncacheRoot(this.animator.gameObject);
@@ -100,7 +100,7 @@ export class ContactShadows extends Behaviour {
100
100
  const obj = new Object3D();
101
101
  obj.name = "ContactShadows";
102
102
  instance = addComponent(obj, ContactShadows, {
103
- autoFit: false,
103
+ autoFit: true,
104
104
  occludeBelowGround: false
105
105
  });
106
106
  this._instances.set(context, instance);
@@ -169,6 +169,10 @@ export class ContactShadows extends Behaviour {
169
169
  return this._needsUpdate;
170
170
  }
171
171
  private _needsUpdate: boolean = false;
172
+ private _needsFit: boolean = false;
173
+
174
+ // TODO: support auto-refit for moveable/animated objects (e.g. via mesh-bvh / scene BVH).
175
+ // Currently there's no reliable way to detect object position/scale changes.
172
176
 
173
177
  /** All shadow objects are parented to this object.
174
178
  * The gameObject itself should not be transformed because we want the ContactShadows object e.g. also have a GroundProjectedEnv component
@@ -366,6 +370,11 @@ export class ContactShadows extends Behaviour {
366
370
 
367
371
  onEnable(): void {
368
372
  this._needsUpdate = true;
373
+ this.autoCleanup(this.context.events.on("scene-content-changed", () => {
374
+ if (!this.autoFit) return;
375
+ this._needsFit = true;
376
+ this._needsUpdate = true;
377
+ }));
369
378
  }
370
379
 
371
380
  /** @internal */
@@ -393,6 +402,11 @@ export class ContactShadows extends Behaviour {
393
402
  /** @internal */
394
403
  onBeforeRender(_frame: XRFrame | null): void {
395
404
 
405
+ if (this._needsFit && this.autoFit) {
406
+ this._needsFit = false;
407
+ this.fitShadows();
408
+ }
409
+
396
410
  if (this.manualUpdate) {
397
411
  if (!this._needsUpdate) return;
398
412
  }
@@ -597,6 +597,9 @@ export class DropListener extends Behaviour {
597
597
  });
598
598
  this.dispatchEvent(evt);
599
599
  this.onDropped?.invoke(evt.detail);
600
+ if (obj) {
601
+ this.context.events.emit("scene-content-changed", { source: this, object: obj });
602
+ }
600
603
 
601
604
  // send network event
602
605
  if (!isRemote && ctx.url?.startsWith("http") && this.context.connection.isConnected && obj) {
@@ -663,7 +663,15 @@ export class OrbitControls extends Behaviour implements ICameraController {
663
663
  }
664
664
  this._controls.enabled = true;
665
665
 
666
- if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
666
+ // Interrupt programmatic transitions on meaningful new user interaction:
667
+ // - Middle/right button down (always intentional camera control)
668
+ // - Mouse wheel (zoom intent)
669
+ // - Left button drag start: getPointerDown(0) with a position delta — a bare click
670
+ // without movement shouldn't cancel an animation, but starting to drag should.
671
+ // Using getPointerDown (not getPointerPressed) ensures we only interrupt once at
672
+ // drag onset, not continuously every frame during a drag.
673
+ const leftDragStart = this.context.input.getPointerDown(0) && (this.context.input.getPointerPositionDelta(0)?.length() || 0) > .1;
674
+ if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || leftDragStart) {
667
675
  this._inputs += 1;
668
676
  }
669
677
  if (this._inputs > 0 && this.allowInterrupt) {
@@ -1095,13 +1103,16 @@ export class OrbitControls extends Behaviour implements ICameraController {
1095
1103
  // Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
1096
1104
  // Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
1097
1105
 
1098
- /**
1099
- * Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
1106
+ /**
1107
+ * Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
1100
1108
  * @param options The options for fitting the camera. Use to provide objects to fit to, fit direction and size and other settings.
1101
1109
  */
1102
1110
  fitCamera(options?: OrbitFitCameraOptions);
1103
- /** @deprecated Use fitCamera(options) */
1104
- fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1111
+ // Deprecated overload commented out: it accepted Object3D as first arg, which caused
1112
+ // TypeScript autocomplete to show Object3D properties (position, worldPosition, etc.)
1113
+ // instead of OrbitFitCameraOptions. The implementation still handles Object3D at runtime
1114
+ // for backwards-compat — use fitCamera({ objects: [...] }) instead.
1115
+ // fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1105
1116
  fitCamera(objectsOrOptions?: Object3D | Array<Object3D> | OrbitFitCameraOptions, options?: OrbitFitCameraOptions): void {
1106
1117
 
1107
1118
 
@@ -747,6 +747,9 @@ export class SceneSwitcher extends Behaviour {
747
747
  const openedEvt = new CustomEvent<LoadSceneEvent>("scene-opened", { detail: { scene: scene, switcher: this, index: index } });
748
748
  this.dispatchEvent(openedEvt);
749
749
  this.sceneLoaded?.invoke(this);
750
+ if (this._currentSceneAsset) {
751
+ this.context.events.emit("scene-content-changed", { source: this, object: this._currentSceneAsset });
752
+ }
750
753
  return true;
751
754
  }
752
755
  }
@@ -34,6 +34,7 @@
34
34
  * @module Built-in Components
35
35
  */
36
36
 
37
+ export { AnimationBuilder, type AnimationKeyframe, type Tween, type AnimationInterpolation } from "./AnimationBuilder.js";
37
38
  export { AnimatorControllerBuilder, type ConditionMode, type StateOptions, type TransitionOptions } from "./AnimatorController.builder.js";
38
39
  export * from "./codegen/components.js";
39
40
  export { Collider } from "./Collider.js"; // export abstract type
@@ -3,10 +3,10 @@
3
3
  export class __Ignore {}
4
4
  export { AlignmentConstraint } from "../AlignmentConstraint.js";
5
5
  export { Animation } from "../Animation.js";
6
+ export { AnimationBuilder } from "../AnimationBuilder.js";
6
7
  export { Keyframe } from "../AnimationCurve.js";
7
8
  export { AnimationCurve } from "../AnimationCurve.js";
8
9
  export { Animator } from "../Animator.js";
9
- export { AnimatorControllerBuilder } from "../AnimatorController.builder.js";
10
10
  export { AnimatorController } from "../AnimatorController.js";
11
11
  export { AudioListener } from "../AudioListener.js";
12
12
  export { AudioSource } from "../AudioSource.js";
@@ -161,12 +161,12 @@ export { SignalAsset } from "../timeline/SignalAsset.js";
161
161
  export { SignalReceiverEvent } from "../timeline/SignalAsset.js";
162
162
  export { SignalReceiver } from "../timeline/SignalAsset.js";
163
163
  export { TimelineBuilder } from "../timeline/TimelineBuilder.js";
164
- export { AnimationTrackHandler } from "../timeline/TimelineTracks.js";
165
- export { AudioTrackHandler } from "../timeline/TimelineTracks.js";
166
- export { MarkerTrackHandler } from "../timeline/TimelineTracks.js";
164
+ export { TimelineAnimationTrack } from "../timeline/TimelineTracks.js";
165
+ export { TimelineAudioTrack } from "../timeline/TimelineTracks.js";
166
+ export { TimelineMarkerTrack } from "../timeline/TimelineTracks.js";
167
167
  export { SignalTrackHandler } from "../timeline/TimelineTracks.js";
168
- export { ActivationTrackHandler } from "../timeline/TimelineTracks.js";
169
- export { ControlTrackHandler } from "../timeline/TimelineTracks.js";
168
+ export { TimelineActivationTrack } from "../timeline/TimelineTracks.js";
169
+ export { TimelineControlTrack } from "../timeline/TimelineTracks.js";
170
170
  export { TransformGizmo } from "../TransformGizmo.js";
171
171
  export { BaseUIComponent } from "../ui/BaseUIComponent.js";
172
172
  export { UIRootComponent } from "../ui/BaseUIComponent.js";
@@ -44,7 +44,7 @@ export enum ClipExtrapolation {
44
44
  };
45
45
 
46
46
  /** @internal */
47
- export type CreateTrackFunction = (director: PlayableDirector, track: Models.TrackModel) => Tracks.TrackHandler | undefined | null;
47
+ export type CreateTrackFunction = (director: PlayableDirector, track: Models.TrackModel) => Tracks.TimelineTrackHandler | undefined | null;
48
48
 
49
49
  /**
50
50
  * PlayableDirector is the main component for controlling timelines in Needle Engine.
@@ -385,7 +385,7 @@ export class PlayableDirector extends Behaviour {
385
385
  /**
386
386
  * @returns all audio tracks of the timeline
387
387
  */
388
- get audioTracks(): Tracks.AudioTrackHandler[] {
388
+ get audioTracks(): Tracks.TimelineAudioTrack[] {
389
389
  return this._audioTracks;
390
390
  }
391
391
 
@@ -399,21 +399,21 @@ export class PlayableDirector extends Behaviour {
399
399
  /**
400
400
  * @returns all marker tracks of the timeline
401
401
  */
402
- get markerTracks(): Tracks.MarkerTrackHandler[] {
402
+ get markerTracks(): Tracks.TimelineMarkerTrack[] {
403
403
  return this._markerTracks;
404
404
  }
405
405
 
406
406
  /**
407
407
  * @returns all activation tracks of the timeline
408
408
  */
409
- get activationTracks(): Tracks.ActivationTrackHandler[] {
409
+ get activationTracks(): Tracks.TimelineActivationTrack[] {
410
410
  return this._activationTracks;
411
411
  }
412
412
 
413
413
  /**
414
414
  * @returns all tracks of the timeline
415
415
  */
416
- get tracks(): ReadonlyArray<Tracks.TrackHandler> {
416
+ get tracks(): ReadonlyArray<Tracks.TimelineTrackHandler> {
417
417
  return this._allTracks.flat();
418
418
  }
419
419
 
@@ -452,16 +452,16 @@ export class PlayableDirector extends Behaviour {
452
452
  private _time: number = 0;
453
453
  private _duration: number = 0;
454
454
  private _weight: number = 1;
455
- private readonly _animationTracks: Array<Tracks.AnimationTrackHandler> = [];
456
- private readonly _audioTracks: Array<Tracks.AudioTrackHandler> = [];
455
+ private readonly _animationTracks: Array<Tracks.TimelineAnimationTrack> = [];
456
+ private readonly _audioTracks: Array<Tracks.TimelineAudioTrack> = [];
457
457
  private readonly _signalTracks: Array<Tracks.SignalTrackHandler> = [];
458
- private readonly _markerTracks: Array<Tracks.MarkerTrackHandler> = [];
459
- private readonly _controlTracks: Array<Tracks.ControlTrackHandler> = [];
460
- private readonly _activationTracks: Array<Tracks.ActivationTrackHandler> = [];
461
- private readonly _customTracks: Array<Tracks.TrackHandler> = [];
458
+ private readonly _markerTracks: Array<Tracks.TimelineMarkerTrack> = [];
459
+ private readonly _controlTracks: Array<Tracks.TimelineControlTrack> = [];
460
+ private readonly _activationTracks: Array<Tracks.TimelineActivationTrack> = [];
461
+ private readonly _customTracks: Array<Tracks.TimelineTrackHandler> = [];
462
462
 
463
- private readonly _tracksArray: Array<Array<Tracks.TrackHandler>> = [];
464
- private get _allTracks(): Array<Array<Tracks.TrackHandler>> {
463
+ private readonly _tracksArray: Array<Array<Tracks.TimelineTrackHandler>> = [];
464
+ private get _allTracks(): Array<Array<Tracks.TimelineTrackHandler>> {
465
465
  this._tracksArray.length = 0;
466
466
  this._tracksArray.push(this._animationTracks);
467
467
  this._tracksArray.push(this._audioTracks);
@@ -535,7 +535,7 @@ export class PlayableDirector extends Behaviour {
535
535
  // When timeline reaches the end "stop()" is called which is evaluating with time 0
536
536
  // We don't want to re-evaluate the animation then in case the timeline is blended with the Animator
537
537
  // e.g then the timeline animation at time 0 is 100% applied on top of the animator animation
538
- if (this._isStopping && handler instanceof Tracks.AnimationTrackHandler) {
538
+ if (this._isStopping && handler instanceof Tracks.TimelineAnimationTrack) {
539
539
  continue;
540
540
  }
541
541
  handler.evaluate(time);
@@ -652,7 +652,7 @@ export class PlayableDirector extends Behaviour {
652
652
  const type = track.type;
653
653
  const registered = PlayableDirector.createTrackFunctions[type];
654
654
  if (registered !== null && registered !== undefined) {
655
- const res = registered(this, track) as Tracks.TrackHandler;
655
+ const res = registered(this, track) as Tracks.TimelineTrackHandler;
656
656
  if (typeof res.evaluate === "function") {
657
657
  res.director = this;
658
658
  res.track = track;
@@ -675,7 +675,7 @@ export class PlayableDirector extends Behaviour {
675
675
  }
676
676
  const animationClips = binding?.gameObject?.animations;
677
677
  if (animationClips) {
678
- const handler = new Tracks.AnimationTrackHandler();
678
+ const handler = new Tracks.TimelineAnimationTrack();
679
679
  handler.trackOffset = track.trackOffset;
680
680
  handler.director = this;
681
681
  handler.track = track;
@@ -725,7 +725,7 @@ export class PlayableDirector extends Behaviour {
725
725
  }
726
726
  else if (track.type === Models.TrackType.Audio) {
727
727
  if (!track.clips || track.clips.length <= 0) continue;
728
- const audio = new Tracks.AudioTrackHandler();
728
+ const audio = new Tracks.TimelineAudioTrack();
729
729
  audio.director = this;
730
730
  audio.track = track;
731
731
  audio.audioSource = track.outputs.find(o => o instanceof AudioSource) as AudioSource;
@@ -749,7 +749,7 @@ export class PlayableDirector extends Behaviour {
749
749
  signalHandler.director = this;
750
750
  signalHandler.track = track;
751
751
 
752
- const markerHandler: Tracks.MarkerTrackHandler = new Tracks.MarkerTrackHandler();
752
+ const markerHandler: Tracks.TimelineMarkerTrack = new Tracks.TimelineMarkerTrack();
753
753
  markerHandler.director = this;
754
754
  markerHandler.track = track;
755
755
 
@@ -795,7 +795,7 @@ export class PlayableDirector extends Behaviour {
795
795
  this._signalTracks.push(handler);
796
796
  }
797
797
  else if (track.type === Models.TrackType.Control) {
798
- const handler = new Tracks.ControlTrackHandler();
798
+ const handler = new Tracks.TimelineControlTrack();
799
799
  handler.director = this;
800
800
  handler.track = track;
801
801
  if (track.clips) {
@@ -807,7 +807,7 @@ export class PlayableDirector extends Behaviour {
807
807
  this._controlTracks.push(handler);
808
808
  }
809
809
  else if (track.type === Models.TrackType.Activation) {
810
- const handler = new Tracks.ActivationTrackHandler();
810
+ const handler = new Tracks.TimelineActivationTrack();
811
811
  handler.director = this;
812
812
  handler.track = track;
813
813
  this._activationTracks.push(handler);