@needle-tools/engine 4.9.0 → 4.9.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 (30) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/{needle-engine.bundle-DrlDKOar.umd.cjs → needle-engine.bundle-BuTUhZAc.umd.cjs} +89 -89
  3. package/dist/{needle-engine.bundle-B1gr_nQ0.min.js → needle-engine.bundle-DoywABnJ.min.js} +95 -95
  4. package/dist/{needle-engine.bundle-BikYBC35.js → needle-engine.bundle-O7rlGMn7.js} +1988 -1948
  5. package/dist/needle-engine.js +2 -2
  6. package/dist/needle-engine.min.js +1 -1
  7. package/dist/needle-engine.umd.cjs +1 -1
  8. package/lib/engine/engine_gameobject.d.ts +7 -7
  9. package/lib/engine/engine_gameobject.js +88 -27
  10. package/lib/engine/engine_gameobject.js.map +1 -1
  11. package/lib/engine/engine_networking_instantiate.js +23 -6
  12. package/lib/engine/engine_networking_instantiate.js.map +1 -1
  13. package/lib/engine/engine_serialization_core.d.ts +2 -2
  14. package/lib/engine/engine_serialization_core.js +7 -7
  15. package/lib/engine/engine_serialization_core.js.map +1 -1
  16. package/lib/engine/engine_serialization_decorator.js +2 -1
  17. package/lib/engine/engine_serialization_decorator.js.map +1 -1
  18. package/lib/engine-components/Renderer.d.ts +7 -6
  19. package/lib/engine-components/Renderer.js.map +1 -1
  20. package/lib/engine-components/web/ScrollFollow.d.ts +19 -1
  21. package/lib/engine-components/web/ScrollFollow.js +20 -1
  22. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  23. package/package.json +1 -1
  24. package/plugins/vite/build.js +3 -0
  25. package/src/engine/engine_gameobject.ts +105 -38
  26. package/src/engine/engine_networking_instantiate.ts +21 -6
  27. package/src/engine/engine_serialization_core.ts +9 -9
  28. package/src/engine/engine_serialization_decorator.ts +2 -1
  29. package/src/engine-components/Renderer.ts +12 -10
  30. package/src/engine-components/web/ScrollFollow.ts +20 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "4.9.0",
3
+ "version": "4.9.1",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.",
5
5
  "main": "dist/needle-engine.min.js",
6
6
  "exports": {
@@ -1,4 +1,7 @@
1
1
 
2
+ /**
3
+ * @returns {import('vite').Plugin}
4
+ */
2
5
  export const needleBuild = (command, config, userSettings) => {
3
6
 
4
7
  // TODO: need to set this when building a dist
@@ -1,4 +1,4 @@
1
- import { Bone, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three";
1
+ import { Bone, Euler, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three";
2
2
 
3
3
  import { $shadowDomOwner } from "../engine-components/ui/Symbols.js";
4
4
  import { type AssetReference } from "./engine_addressables.js";
@@ -11,7 +11,7 @@ import { processNewScripts } from "./engine_mainloop_utils.js";
11
11
  import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
12
12
  import { assign, ISerializable } from "./engine_serialization_core.js";
13
13
  import { Context, registerComponent } from "./engine_setup.js";
14
- import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
14
+ import { getTempQuaternion, logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
15
15
  import { type Constructor, type GuidsMap, type IComponent as Component, type IComponent, IEventList, type IGameObject as GameObject, type UIDProvider } from "./engine_types.js";
16
16
  import { deepClone, getParam, tryFindObject } from "./engine_utils.js";
17
17
  import { apply } from "./js-extensions/index.js";
@@ -24,12 +24,12 @@ export type IInstantiateOptions = {
24
24
  //** parent guid or object */
25
25
  parent?: string | Object3D;
26
26
  /** position in local space. Set `keepWorldPosition` to true if this is world space */
27
- position?: Vector3;
27
+ position?: Vector3 | [number, number, number];
28
28
  /** for duplicatable parenting */
29
29
  keepWorldPosition?: boolean;
30
30
  /** rotation in local space. Set `keepWorldPosition` to true if this is world space */
31
- rotation?: Quaternion;
32
- scale?: Vector3;
31
+ rotation?: Quaternion | Euler | [number, number, number];
32
+ scale?: Vector3 | [number, number, number];
33
33
  /** if the instantiated object should be visible */
34
34
  visible?: boolean;
35
35
  context?: Context;
@@ -46,9 +46,9 @@ export class InstantiateOptions implements IInstantiateOptions {
46
46
  idProvider?: UIDProvider | undefined;
47
47
  parent?: string | undefined | Object3D;
48
48
  keepWorldPosition?: boolean
49
- position?: Vector3 | undefined;
50
- rotation?: Quaternion | undefined;
51
- scale?: Vector3 | undefined;
49
+ position?: Vector3 | [number, number, number] | undefined;
50
+ rotation?: Quaternion | Euler | [number, number, number] | undefined;
51
+ scale?: Vector3 | [number, number, number] | undefined;
52
52
  visible?: boolean | undefined;
53
53
  context?: Context | undefined;
54
54
  components?: boolean | undefined;
@@ -58,9 +58,9 @@ export class InstantiateOptions implements IInstantiateOptions {
58
58
  clone.idProvider = this.idProvider;
59
59
  clone.parent = this.parent;
60
60
  clone.keepWorldPosition = this.keepWorldPosition;
61
- clone.position = this.position?.clone();
62
- clone.rotation = this.rotation?.clone();
63
- clone.scale = this.scale?.clone();
61
+ clone.position = Array.isArray(this.position) ? [...this.position] : this.position?.clone();
62
+ clone.rotation = Array.isArray(this.rotation) ? [...this.rotation] : this.rotation?.clone();
63
+ clone.scale = Array.isArray(this.scale) ? [...this.scale] : this.scale?.clone();
64
64
  clone.visible = this.visible;
65
65
  clone.context = this.context;
66
66
  clone.components = this.components;
@@ -72,9 +72,9 @@ export class InstantiateOptions implements IInstantiateOptions {
72
72
  this.idProvider = other.idProvider;
73
73
  this.parent = other.parent;
74
74
  this.keepWorldPosition = other.keepWorldPosition;
75
- this.position = other.position?.clone();
76
- this.rotation = other.rotation?.clone();
77
- this.scale = other.scale?.clone();
75
+ this.position = Array.isArray(other.position) ? [...other.position] : other.position?.clone();
76
+ this.rotation = Array.isArray(other.rotation) ? [...other.rotation] : other.rotation?.clone();
77
+ this.scale = Array.isArray(other.scale) ? [...other.scale] : other.scale?.clone();
78
78
  this.visible = other.visible;
79
79
  this.context = other.context;
80
80
  this.components = other.components;
@@ -287,6 +287,8 @@ declare type ObjectCloneReference = {
287
287
 
288
288
 
289
289
  declare type InstantiateReferenceMap = Record<string, ObjectCloneReference>;
290
+ declare type NewObjectReferenceMap = Record<string, { target: object, key: string }>;
291
+
290
292
  /**
291
293
  * Provides access to the instantiated object and its clone
292
294
  */
@@ -325,13 +327,13 @@ export function instantiate(instance: AssetReference | GameObject | Object3D, op
325
327
  }
326
328
 
327
329
  const components: Array<Component> = [];
328
- const goMapping: InstantiateReferenceMap = {}; // used to resolve references on components to components on other gameobjects to their new counterpart
330
+ const referencemap: InstantiateReferenceMap = {}; // used to resolve references on components to components on other gameobjects to their new counterpart
329
331
  const skinnedMeshes: InstantiateReferenceMap = {}; // used to resolve skinned mesh bones
330
- const clone = internalInstantiate(context, instance, options, components, goMapping, skinnedMeshes);
332
+ const clone = internalInstantiate(context, instance, options, components, referencemap, skinnedMeshes);
331
333
 
332
334
  if (clone) {
333
- resolveReferences(goMapping);
334
- resolveAndBindSkinnedMeshBones(skinnedMeshes, goMapping);
335
+ resolveReferences(clone, referencemap);
336
+ resolveAndBindSkinnedMeshBones(skinnedMeshes, referencemap);
335
337
  }
336
338
 
337
339
  if (debug) {
@@ -426,19 +428,45 @@ function internalInstantiate(
426
428
  parent.add(clone);
427
429
  }
428
430
 
429
- // apply transform
431
+ // POSITION
430
432
  if (opts?.position) {
431
- setWorldPosition(clone, opts.position);
433
+ if (Array.isArray(opts.position)) {
434
+ const vec = new Vector3();
435
+ vec.fromArray(opts.position);
436
+ clone.worldPosition = vec;
437
+ }
438
+ else {
439
+ clone.worldPosition = opts.position;
440
+ }
432
441
  }
433
442
  else clone.position.copy(instance.position);
443
+
444
+ // ROTATION
434
445
  if (opts?.rotation) {
435
- setWorldQuaternion(clone, opts.rotation);
446
+ if (opts.rotation instanceof Quaternion)
447
+ clone.worldQuaternion = opts.rotation;
448
+ else if (opts.rotation instanceof Euler)
449
+ clone.worldQuaternion = getTempQuaternion().setFromEuler(opts.rotation);
450
+ else if (Array.isArray(opts.rotation)) {
451
+ const euler = new Euler();
452
+ euler.fromArray(opts.rotation);
453
+ clone.worldQuaternion = getTempQuaternion().setFromEuler(euler);
454
+ }
436
455
  }
437
456
  else clone.quaternion.copy(instance.quaternion);
457
+
458
+ // SCALE
438
459
  if (opts?.scale) {
439
- clone.scale.copy(opts.scale);
440
460
  // TODO MAJOR: replace with worldscale
441
461
  // clone.worldScale = opts.scale;
462
+ if (Array.isArray(opts.scale)) {
463
+ const vec = new Vector3();
464
+ vec.fromArray(opts.scale);
465
+ opts.scale = vec;
466
+ }
467
+ else {
468
+ clone.scale.copy(opts.scale);
469
+ }
442
470
  }
443
471
  else clone.scale.copy(instance.scale);
444
472
 
@@ -470,17 +498,7 @@ function internalInstantiate(
470
498
  for (let i = 0; i < components.length; i++) {
471
499
  const comp = components[i];
472
500
  const copy = new comp.constructor();
473
- assign(copy, comp, undefined, {
474
- // onAssign: (source, key, value) => {
475
- // if (typeof value === "object") {
476
- // const serializable = source as ISerializable;
477
- // if (serializable?.$serializedTypes?.[key]) {
478
- // console.debug("TODO CLONE", key, value);
479
- // }
480
- // }
481
- // return value;
482
- // }
483
- });
501
+ onAssignComponent(comp, copy, objectsMap);
484
502
  // make sure the original guid stays intact
485
503
  if (comp[editorGuidKeyName] !== undefined)
486
504
  copy[editorGuidKeyName] = comp[editorGuidKeyName];
@@ -501,7 +519,6 @@ function internalInstantiate(
501
519
  opts.parent = undefined;
502
520
  opts.visible = undefined;
503
521
  }
504
-
505
522
  for (const ch in instance.children) {
506
523
  const child = instance.children[ch];
507
524
  const newChild = internalInstantiate(context, child as GameObject, opts, componentsList, objectsMap, skinnedMeshesMap);
@@ -510,11 +527,59 @@ function internalInstantiate(
510
527
  clone.add(newChild);
511
528
  }
512
529
  }
513
-
514
530
  return clone;
531
+ }
532
+
515
533
 
534
+ function onAssignComponent(source: any, target: any, _newObjectsMap: InstantiateReferenceMap) {
535
+ assign(target, source, undefined, {
536
+ // onAssigned: (target, key, _oldValue, value) => {
537
+ // if (value !== null && typeof value === "object") {
538
+ // const serializable = target as ISerializable;
539
+ // if (serializable?.$serializedTypes?.[key]) {
540
+ // if (!(value instanceof Object3D)) {
541
+ // // let clone = null;
542
+ // // if ("clone" in value) {
543
+ // // if (canClone(value)) clone = (value as any).clone();
544
+ // // }
545
+ // // else {
546
+ // // clone = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
547
+ // // }
548
+ // // if (clone) {
549
+ // // console.debug(key, { target, value, clone })
550
+ // // target[key] = clone;
551
+ // // findNestedReferences(clone, objectsMap);
552
+ // // }
553
+ // // else console.debug("Could not clone value for key", key, value);
554
+ // }
555
+ // else {
556
+ // console.log("ASSIGNED", value)
557
+ // }
558
+
559
+ // recursiveAssign(target, target[key], newObjectsMap);
560
+ // }
561
+
562
+ // }
563
+ // }
564
+ });
516
565
  }
517
566
 
567
+ // function findNestedReferences(object: object, map: InstantiateReferenceMap) {
568
+ // const keys = Object.keys(object);
569
+ // for (const key of keys) {
570
+ // const val = (object as any)[key];
571
+ // if (val instanceof Object3D) {
572
+ // if ("guid" in val && val.guid) {
573
+ // console.log("FOUND ", val.guid, val)
574
+ // map[val.guid] = { original: val, clone: null };
575
+ // }
576
+ // }
577
+ // else if (typeof val === "object" && val !== null) {
578
+ // findNestedReferences(val, map);
579
+ // }
580
+ // }
581
+ // }
582
+
518
583
  function resolveAndBindSkinnedMeshBones(
519
584
  skinnedMeshes: { [key: string]: ObjectCloneReference },
520
585
  newObjectsMap: { [key: string]: ObjectCloneReference }
@@ -599,13 +664,13 @@ function resolveAndBindSkinnedMeshBones(
599
664
 
600
665
  // }
601
666
 
602
- function resolveReferences(newObjectsMap: InstantiateReferenceMap) {
667
+ function resolveReferences(_newInstance: Object3D, newObjectsMap: InstantiateReferenceMap) {
603
668
  // for every object that is newly created we want to update references to their newly created counterparts
604
669
  // e.g. a collider instance referencing a rigidbody instance should be updated so that
605
670
  // the cloned collider does not reference the cloned rigidbody (instead of the original rigidbody)
606
671
  for (const key in newObjectsMap) {
607
672
  const val = newObjectsMap[key];
608
- const clone = val.clone as Object3D;
673
+ const clone = val.clone as Object3D | null;
609
674
  // resolve references
610
675
  if (clone?.isObject3D && clone?.userData?.components) {
611
676
  for (let i = 0; i < clone.userData.components.length; i++) {
@@ -706,4 +771,6 @@ function postProcessNewInstance(copy: Object3D, key: string, value: IComponent |
706
771
  return copy;
707
772
  }
708
773
  }
709
- }
774
+ }
775
+
776
+ // const canClone = (value: any) => value.isVector4 || value.isVector3 || value.isVector2 || value.isQuaternion || value.isEuler || value.isColor === true;
@@ -1,4 +1,4 @@
1
- import { Object3D, Quaternion, Vector3 } from "three";
1
+ import { Euler, Object3D, Quaternion, Vector3 } from "three";
2
2
  // https://github.com/uuidjs/uuid
3
3
  // v5 takes string and namespace
4
4
  import { v5 } from 'uuid';
@@ -254,12 +254,27 @@ export function syncInstantiate(object: GameObject | Object3D, opts: SyncInstant
254
254
  if (opts.deleteOnDisconnect === true)
255
255
  model.deleteStateOnDisconnect = true;
256
256
  if (originalOpts) {
257
- if (originalOpts.position)
258
- model.position = { x: originalOpts.position.x, y: originalOpts.position.y, z: originalOpts.position.z };
259
- if (originalOpts.rotation)
257
+ if (originalOpts.position) {
258
+ if (Array.isArray(originalOpts.position)) {
259
+ model.position = { x: originalOpts.position[0], y: originalOpts.position[1], z: originalOpts.position[2] };
260
+ }
261
+ else model.position = { x: originalOpts.position.x, y: originalOpts.position.y, z: originalOpts.position.z };
262
+ }
263
+ if (originalOpts.rotation) {
264
+ if (originalOpts.rotation instanceof Euler) {
265
+ originalOpts.rotation = new Quaternion().setFromEuler(originalOpts.rotation);
266
+ }
267
+ else if (originalOpts.rotation instanceof Array) {
268
+ originalOpts.rotation = new Quaternion().fromArray(originalOpts.rotation);
269
+ }
260
270
  model.rotation = { x: originalOpts.rotation.x, y: originalOpts.rotation.y, z: originalOpts.rotation.z, w: originalOpts.rotation.w };
261
- if (originalOpts.scale)
262
- model.scale = { x: originalOpts.scale.x, y: originalOpts.scale.y, z: originalOpts.scale.z };
271
+ }
272
+ if (originalOpts.scale) {
273
+ if (Array.isArray(originalOpts.scale)) {
274
+ model.scale = { x: originalOpts.scale[0], y: originalOpts.scale[1], z: originalOpts.scale[2] };
275
+ }
276
+ else model.scale = { x: originalOpts.scale.x, y: originalOpts.scale.y, z: originalOpts.scale.z };
277
+ }
263
278
  }
264
279
  if (!model.position)
265
280
  model.position = { x: go.position.x, y: go.position.y, z: go.position.z };
@@ -658,10 +658,10 @@ export const $isAssigningProperties = Symbol("assigned component properties");
658
658
  * @param key the key that is being assigned
659
659
  * @param value the value that is being assigned
660
660
  */
661
- type OnAssign = (source: object, key: string, value: any) => any;
661
+ type AssignedCallback = (source: object, key: string, oldValue: any, newValue: any) => any;
662
662
 
663
663
  /** Object.assign behaviour but check if property is writeable (e.g. getter only properties are skipped) */
664
- export function assign(target: any, source: any, info?: ImplementationInformation, opts?: { onAssign?: OnAssign }) {
664
+ export function assign(target: any, source: any, info?: ImplementationInformation, opts?: { onAssigned?: AssignedCallback }) {
665
665
  if (source === undefined || source === null) return;
666
666
  if (target === undefined || target === null) return;
667
667
 
@@ -699,13 +699,13 @@ export function assign(target: any, source: any, info?: ImplementationInformatio
699
699
  // arrow functions are defined as properties on the object
700
700
  continue;
701
701
  }
702
- if (!desc || desc.writable === true) {
703
- const value = opts?.onAssign ? opts.onAssign(source, key, source[key]) : source[key];
704
- target[key] = value;
705
- }
706
- else if (desc?.set !== undefined) {
707
- const value = opts?.onAssign ? opts.onAssign(source, key, source[key]) : source[key];
708
- target[key] = value;
702
+ if (!desc || desc.writable === true || desc.set !== undefined) {
703
+ const newValue = source[key];
704
+ const oldValue = target[key];
705
+ target[key] = newValue;
706
+ if (opts?.onAssigned) {
707
+ opts.onAssigned(target, key, oldValue, newValue);
708
+ }
709
709
  }
710
710
  }
711
711
  delete target[$isAssigningProperties];
@@ -27,7 +27,8 @@ export const serializable = function <T>(type?: Constructor<T> | null | Array<Co
27
27
 
28
28
  return function (_target: any, _propertyKey: string | { name: string }) {
29
29
  if (!_target) {
30
- console.error("Found @serializable decorator without a target");
30
+ const propertyName = typeof _propertyKey === 'string' ? _propertyKey : _propertyKey.name;
31
+ console.warn(`@serializable without a target at '${propertyName}'.`);
31
32
  return;
32
33
  }
33
34
  // The _propertyKey parameter is a string in TS4 with experimentalDecorators
@@ -1,5 +1,5 @@
1
1
  import { getRaycastMesh } from "@needle-tools/gltf-progressive";
2
- import { AxesHelper, Material, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
2
+ import { AxesHelper, Material, Mesh, MeshBasicMaterial, MeshPhysicalMaterial, MeshStandardMaterial, Object3D, RawShaderMaterial, ShaderMaterial, SkinnedMesh, Texture, Vector4 } from "three";
3
3
 
4
4
  import { showBalloonWarning } from "../engine/debug/index.js";
5
5
  import { getComponent, getOrAddComponent } from "../engine/engine_components.js";
@@ -48,6 +48,8 @@ export enum RenderState {
48
48
  Front = 2,
49
49
  }
50
50
 
51
+ type SharedMaterial = (Material & Partial<MeshStandardMaterial> & Partial<MeshPhysicalMaterial> & Partial<ShaderMaterial> & Partial<RawShaderMaterial>);
52
+
51
53
 
52
54
  // support sharedMaterials[index] assigning materials directly to the objects
53
55
  class SharedMaterialArray implements ISharedMaterials {
@@ -185,7 +187,7 @@ class SharedMaterialArray implements ISharedMaterials {
185
187
  this.changed = true;
186
188
  }
187
189
 
188
- private getMaterial(index: number): Material | null {
190
+ private getMaterial(index: number): SharedMaterial | null {
189
191
  index = this.resolveIndex(index);
190
192
  if (index < 0) return null;
191
193
  const obj = this._targets;
@@ -302,11 +304,11 @@ export class Renderer extends Behaviour implements IRenderer {
302
304
  return this._sharedMeshes;
303
305
  }
304
306
 
305
- get sharedMaterial(): Material {
306
- return this.sharedMaterials[0];
307
+ get sharedMaterial(): SharedMaterial {
308
+ return this.sharedMaterials[0] as SharedMaterial;
307
309
  }
308
310
 
309
- set sharedMaterial(mat: Material) {
311
+ set sharedMaterial(mat: SharedMaterial) {
310
312
  const cur = this.sharedMaterials[0];
311
313
  if (cur === mat) return;
312
314
  this.sharedMaterials[0] = mat;
@@ -314,12 +316,12 @@ export class Renderer extends Behaviour implements IRenderer {
314
316
  }
315
317
 
316
318
  /**@deprecated please use sharedMaterial */
317
- get material(): Material {
318
- return this.sharedMaterials[0];
319
+ get material(): SharedMaterial {
320
+ return this.sharedMaterials[0] as SharedMaterial;
319
321
  }
320
322
 
321
323
  /**@deprecated please use sharedMaterial */
322
- set material(mat: Material) {
324
+ set material(mat: SharedMaterial) {
323
325
  this.sharedMaterial = mat;
324
326
  }
325
327
 
@@ -329,10 +331,10 @@ export class Renderer extends Behaviour implements IRenderer {
329
331
  private _probeAnchorLastFrame?: Object3D;
330
332
 
331
333
  // this is just available during deserialization
332
- private set sharedMaterials(_val: Array<Material | null>) {
334
+ private set sharedMaterials(_val: Array<SharedMaterial | null>) {
333
335
  // TODO: elements in the array might be missing at the moment which leads to problems if an index is serialized
334
336
  if (!this._originalMaterials) {
335
- this._originalMaterials = _val as Material[];
337
+ this._originalMaterials = _val as SharedMaterial[];
336
338
  }
337
339
  else if (_val) {
338
340
  let didWarn = false;
@@ -28,18 +28,36 @@ type ScrollFollowEvent = {
28
28
  * The ScrollFollow component allows you to link the scroll position of the page (or a specific element) to one or more target objects.
29
29
  * This can be used to create scroll-based animations, audio playback, or other effects. For example you can link the scroll position to a timeline (PlayableDirector) to create scroll-based storytelling effects or to an Animator component to change the animation state based on scroll.
30
30
  *
31
+ * Assign {@link target} objects to the component to have them updated based on the current scroll position (check the 'target' property for supported types).
32
+ *
31
33
  * @link Example at https://scrollytelling-2-z23hmxby7c6x-u30ld.needle.run/
34
+ * @link Template at https://github.com/needle-engine/scrollytelling-template
35
+ *
36
+ * ## How to use with an Animator
37
+ * 1. Create an Animator component and set up a float parameter named "scroll".
38
+ * 2. Create transitions between animation states based on the "scroll" parameter (e.g. from 0 to 1).
39
+ * 3. Add a ScrollFollow component to the same GameObject or another GameObject in the scene.
40
+ * 4. Assign the Animator component to the ScrollFollow's target property.
41
+ *
42
+ * ## How to use with a PlayableDirector (timeline)
43
+ * 1. Create a PlayableDirector component and set up a timeline asset.
44
+ * 2. Add a ScrollFollow component to the same GameObject or another GameObject in the scene.
45
+ * 3. Assign the PlayableDirector component to the ScrollFollow's target property.
46
+ * 4. The timeline will now scrub based on the scroll position of the page.
32
47
  */
33
48
  export class ScrollFollow extends Behaviour {
34
49
 
35
50
  /**
36
- * Target object(s) to follow the scroll position of the page. If null, the main camera is used.
51
+ * Target object(s) to follow the scroll position of the page.
37
52
  *
38
53
  * Supported target types:
39
54
  * - PlayableDirector (timeline), the scroll position will be mapped to the timeline time
40
55
  * - Animator, the scroll position will be set to a float parameter named "scroll"
41
56
  * - Animation, the scroll position will be mapped to the animation time
42
57
  * - AudioSource, the scroll position will be mapped to the audio time
58
+ * - SplineWalker, the scroll position will be mapped to the position01 property
59
+ * - Light, the scroll position will be mapped to the intensity property
60
+ * - Object3D, the object will move vertically based on the scroll position
43
61
  * - Any object with a `scroll` property (number or function)
44
62
  */
45
63
  @serializable([Behaviour, Object3D])
@@ -204,6 +222,7 @@ export class ScrollFollow extends Behaviour {
204
222
  }
205
223
  const bounds = target["needle:scrollbounds"] as Box3;
206
224
  if (bounds) {
225
+ // TODO: remap position to use upper screen edge and lower edge instead of center
207
226
  target.position.y = -bounds.min.y - value * (bounds.max.y - bounds.min.y);
208
227
  }
209
228
  }