@needle-tools/engine 4.8.9 → 4.9.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 (96) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/gltf-progressive-DWiyqrwB.umd.cjs +8 -0
  3. package/dist/gltf-progressive-DhE1A6hX.min.js +8 -0
  4. package/dist/{gltf-progressive-BcHT3Nyo.js → gltf-progressive-egsMzRdv.js} +384 -369
  5. package/dist/{needle-engine.bundle-BdgNRB4B.js → needle-engine.bundle-CVeZ2dVS.js} +8003 -7731
  6. package/dist/{needle-engine.bundle-DGca1cic.umd.cjs → needle-engine.bundle-CokWPL7y.umd.cjs} +138 -138
  7. package/dist/{needle-engine.bundle-TQyyn3pE.min.js → needle-engine.bundle-DW-9zmeW.min.js} +142 -142
  8. package/dist/needle-engine.js +305 -301
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/dist/{postprocessing-Ywv5oKkX.min.js → postprocessing-B_FzkwDz.min.js} +1 -1
  12. package/dist/{postprocessing-ORx-0eCx.js → postprocessing-DP1U_BpT.js} +2 -2
  13. package/dist/{postprocessing-CVb_x9YY.umd.cjs → postprocessing-DTX5VXrj.umd.cjs} +1 -1
  14. package/dist/rapier-BJaux8TQ.js +5217 -0
  15. package/dist/rapier-Bd0qRV1r.umd.cjs +1 -0
  16. package/dist/rapier-CnHGx3sO.min.js +1 -0
  17. package/dist/{three-Dceyffus.umd.cjs → three-BK56xWDs.umd.cjs} +24 -24
  18. package/dist/{three-BRSLmpyi.js → three-CsHK73Zc.js} +1 -0
  19. package/dist/{three-CsmWHVn7.min.js → three-TNFQHSFa.min.js} +25 -25
  20. package/dist/{three-examples-BX_Sktc9.min.js → three-examples-Bph291U2.min.js} +1 -1
  21. package/dist/{three-examples-CNexix3E.js → three-examples-BvMpKSun.js} +1 -1
  22. package/dist/{three-examples-DWxXVnws.umd.cjs → three-examples-C9WfZu-X.umd.cjs} +1 -1
  23. package/dist/{three-mesh-ui-gqAXlGNB.js → three-mesh-ui-CN6aRT7i.js} +1 -1
  24. package/dist/{three-mesh-ui-Cdh2iW8b.umd.cjs → three-mesh-ui-DnxkZWNA.umd.cjs} +1 -1
  25. package/dist/{three-mesh-ui-Bwy12Qvg.min.js → three-mesh-ui-n_qS2BM-.min.js} +1 -1
  26. package/dist/{vendor-xfQ8tKF3.umd.cjs → vendor-8le8g4MS.umd.cjs} +1 -1
  27. package/dist/{vendor-Z4SPrTcP.js → vendor-Msb9AgYE.js} +1 -1
  28. package/dist/{vendor-C43vobGc.min.js → vendor-yDFiCnCw.min.js} +1 -1
  29. package/lib/engine/codegen/register_types.js +4 -0
  30. package/lib/engine/codegen/register_types.js.map +1 -1
  31. package/lib/engine/engine_addressables.d.ts +5 -0
  32. package/lib/engine/engine_addressables.js +19 -8
  33. package/lib/engine/engine_addressables.js.map +1 -1
  34. package/lib/engine/engine_assetdatabase.js +10 -8
  35. package/lib/engine/engine_assetdatabase.js.map +1 -1
  36. package/lib/engine/engine_physics_rapier.js +6 -4
  37. package/lib/engine/engine_physics_rapier.js.map +1 -1
  38. package/lib/engine/engine_three_utils.js +4 -2
  39. package/lib/engine/engine_three_utils.js.map +1 -1
  40. package/lib/engine/xr/NeedleXRController.js +30 -9
  41. package/lib/engine/xr/NeedleXRController.js.map +1 -1
  42. package/lib/engine/xr/NeedleXRSession.js +18 -13
  43. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  44. package/lib/engine-components/RendererLightmap.js +3 -1
  45. package/lib/engine-components/RendererLightmap.js.map +1 -1
  46. package/lib/engine-components/api.d.ts +1 -0
  47. package/lib/engine-components/api.js +1 -0
  48. package/lib/engine-components/api.js.map +1 -1
  49. package/lib/engine-components/codegen/components.d.ts +3 -0
  50. package/lib/engine-components/codegen/components.js +3 -0
  51. package/lib/engine-components/codegen/components.js.map +1 -1
  52. package/lib/engine-components/export/usdz/extensions/behavior/Behaviour.d.ts +1 -1
  53. package/lib/engine-components/splines/Spline.d.ts +58 -0
  54. package/lib/engine-components/splines/Spline.js +270 -0
  55. package/lib/engine-components/splines/Spline.js.map +1 -0
  56. package/lib/engine-components/splines/SplineUtils.d.ts +12 -0
  57. package/lib/engine-components/splines/SplineUtils.js +33 -0
  58. package/lib/engine-components/splines/SplineUtils.js.map +1 -0
  59. package/lib/engine-components/splines/SplineWalker.d.ts +47 -0
  60. package/lib/engine-components/splines/SplineWalker.js +113 -0
  61. package/lib/engine-components/splines/SplineWalker.js.map +1 -0
  62. package/lib/engine-components/splines/index.d.ts +3 -0
  63. package/lib/engine-components/splines/index.js +4 -0
  64. package/lib/engine-components/splines/index.js.map +1 -0
  65. package/lib/engine-components/webxr/WebXRImageTracking.d.ts +23 -1
  66. package/lib/engine-components/webxr/WebXRImageTracking.js +57 -3
  67. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  68. package/lib/engine-components/webxr/controllers/XRControllerModel.js +2 -2
  69. package/lib/engine-components/webxr/controllers/XRControllerModel.js.map +1 -1
  70. package/lib/engine-components/webxr/controllers/XRControllerMovement.js +4 -0
  71. package/lib/engine-components/webxr/controllers/XRControllerMovement.js.map +1 -1
  72. package/package.json +3 -3
  73. package/plugins/vite/alias.js +3 -2
  74. package/plugins/vite/peer.js +7 -3
  75. package/src/engine/codegen/register_types.ts +4 -0
  76. package/src/engine/engine_addressables.ts +21 -6
  77. package/src/engine/engine_assetdatabase.ts +9 -7
  78. package/src/engine/engine_physics_rapier.ts +7 -5
  79. package/src/engine/engine_three_utils.ts +5 -2
  80. package/src/engine/xr/NeedleXRController.ts +39 -15
  81. package/src/engine/xr/NeedleXRSession.ts +18 -13
  82. package/src/engine-components/RendererLightmap.ts +2 -1
  83. package/src/engine-components/api.ts +1 -0
  84. package/src/engine-components/codegen/components.ts +3 -0
  85. package/src/engine-components/splines/Spline.ts +284 -0
  86. package/src/engine-components/splines/SplineUtils.ts +32 -0
  87. package/src/engine-components/splines/SplineWalker.ts +106 -0
  88. package/src/engine-components/splines/index.ts +3 -0
  89. package/src/engine-components/webxr/WebXRImageTracking.ts +59 -4
  90. package/src/engine-components/webxr/controllers/XRControllerModel.ts +3 -2
  91. package/src/engine-components/webxr/controllers/XRControllerMovement.ts +5 -1
  92. package/dist/gltf-progressive-CH3Q4H06.umd.cjs +0 -8
  93. package/dist/gltf-progressive-DR6HqF_h.min.js +0 -8
  94. package/dist/rapier--oeYP_h7.umd.cjs +0 -1
  95. package/dist/rapier-B3xpyPtq.js +0 -5142
  96. package/dist/rapier-CyWhltHY.min.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { AnimationAction, Box3, Box3Helper, Camera, Color, DepthTexture, Euler, GridHelper, Layers, Material, Mesh, MeshStandardMaterial, Object3D, OrthographicCamera, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector2Like, Vector3, WebGLRenderTarget } from "three";
2
2
  import { ShaderMaterial, WebGLRenderer } from "three";
3
3
  import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
4
+ import { rendererReference } from "three/src/nodes/TSL.js";
4
5
 
5
6
  import { useForAutoFit } from "./engine_camera.js";
6
7
  import { Mathf } from "./engine_math.js"
@@ -587,8 +588,10 @@ export class Graphics {
587
588
  renderer.state.buffers.depth.setMask(depthWrite);
588
589
 
589
590
  renderer.setClearColor(new Color(0, 0, 0), 0);
590
- // renderer.setSize(target.width, target.height);
591
- renderer.setPixelRatio(window.devicePixelRatio);
591
+ if (renderer.pixelRatio !== window.devicePixelRatio) {
592
+ if (renderer.xr.isPresenting === false)
593
+ renderer.setPixelRatio(window.devicePixelRatio);
594
+ }
592
595
  renderer.setRenderTarget(target);
593
596
  renderer.clear();
594
597
  renderer.render(this.scene, this.perspectiveCam);
@@ -398,8 +398,8 @@ export class NeedleXRController implements IPointerHitEventReceiver {
398
398
  C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inputSource.hand ? "x" : "-"} Pen: ${this._isMxInk ? "x" : "-"}`;
399
399
  if (this.inputSource.hand) debugStr += `\nPinch: ${this.getGesture("pinch")?.value.toFixed(3)}`;
400
400
  debugStr += "\n" + profileStr;
401
- debugStr += "\n" + (this.inputSource.targetRaySpace ? `Ray: x` : "Ray: -") +
402
- (this.inputSource.gripSpace ? " Grip: x" : " Grip: -") +
401
+ debugStr += "\n" + (this.inputSource.targetRaySpace ? `Ray: x` : "Ray: -") +
402
+ (this.inputSource.gripSpace ? " Grip: x" : " Grip: -") +
403
403
  (this.inputSource.gamepad ? ` Gamepad: ${this.inputSource.gamepad.mapping}` : " Gamepad: -");
404
404
  if (this.inputSource.gamepad) {
405
405
  const gp = this.inputSource.gamepad;
@@ -415,7 +415,7 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
415
415
  this._handJointPoses.clear();
416
416
  this._hand_wristDotUp = undefined;
417
417
 
418
- if (!this.xr.referenceSpace) {
418
+ if (!this.xr.referenceSpace || !this.inputSource.gamepad?.connected) {
419
419
  this._isTracking = false;
420
420
  return;
421
421
  }
@@ -460,7 +460,7 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
460
460
 
461
461
  // update controller object parent – needs to be parented to the rig, which
462
462
  // implicitly is the same object as the camera parent.
463
- if (this.xr.context.mainCamera?.parent) {
463
+ if (this.xr.context.mainCamera?.parent) {
464
464
  if (this._object.parent !== this.xr.context.mainCamera?.parent)
465
465
  this.xr.context.mainCamera.parent.add(this._object);
466
466
  if (this._gripSpaceObject !== undefined && this._gripSpaceObject?.parent !== this.xr.context.mainCamera?.parent)
@@ -594,6 +594,24 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
594
594
  return this.getGesture("pinch");
595
595
  }
596
596
  return this.toNeedleGamepadButton(0, key);
597
+
598
+ case "xr-standard-trigger":
599
+ if (this.inputSource.gamepad) {
600
+ return this.toNeedleGamepadButton(0, key);
601
+ }
602
+ break;
603
+
604
+ case "xr-standard-squeeze":
605
+ if (this.inputSource.gamepad) {
606
+ return this.toNeedleGamepadButton(1, key);
607
+ }
608
+ break;
609
+
610
+ case "xr-standard-thumbstick":
611
+ if (this.inputSource.gamepad) {
612
+ return this.toNeedleGamepadButton(3, key);
613
+ }
614
+ break;
597
615
  }
598
616
 
599
617
 
@@ -694,12 +712,18 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
694
712
  getStick(key: StickName | "primary"): Vec3 {
695
713
  if (!this._layout) return { x: 0, y: 0, z: 0 };
696
714
 
715
+ // Hands dont have thumbsicks
716
+ if (this.isHand) {
717
+ return { x: 0, y: 0, z: 0 };
718
+ }
719
+
697
720
  if (key === "primary") {
698
- const x = this.inputSource.gamepad?.axes[0] || 0;
699
- const y = this.inputSource.gamepad?.axes[1] || 0;
700
- // the primary thumbstick is button 3 (see gamepads module explainer)
701
- const z = this.inputSource.gamepad?.buttons[3]?.value || 0;
702
- return { x, y, z }
721
+ if (this._layout.components["xr-standard-thumbstick"]) key = "xr-standard-thumbstick";
722
+ // const x = this.inputSource.gamepad?.axes[0] || 0;
723
+ // const y = this.inputSource.gamepad?.axes[1] || 0;
724
+ // // the primary thumbstick is button 3 (see gamepads module explainer)
725
+ // const z = this.inputSource.gamepad?.buttons[3]?.value || 0;
726
+ // return { x, y, z }
703
727
  }
704
728
 
705
729
  const componentModel = this._layout?.components[key];
@@ -709,12 +733,12 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
709
733
  if (this.inputSource.gamepad) {
710
734
  const xIndex = componentModel.gamepadIndices!.xAxis!;
711
735
  const yIndex = componentModel.gamepadIndices!.yAxis!;
712
- let x = this.inputSource.gamepad?.axes[xIndex];
713
- let y = this.inputSource.gamepad?.axes[yIndex];
736
+ let x = this.inputSource.gamepad.axes[xIndex] || 0;
737
+ let y = this.inputSource.gamepad.axes[yIndex] || 0;
714
738
  x *= -1;
715
739
  y *= -1;
716
740
  const buttonIndex = componentModel.gamepadIndices!.button!;
717
- const z = this.inputSource.gamepad?.buttons[buttonIndex]?.value;
741
+ const z = this.inputSource.gamepad?.buttons[buttonIndex]?.value || 0;
718
742
  return { x, y, z }
719
743
  }
720
744
  }
@@ -731,13 +755,13 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
731
755
  private initialize() {
732
756
  // WORKAROUND for hand controllers that don't have a select event
733
757
  this._hasSelectEvent = this.profiles.includes("generic-hand-select") || this.profiles.some(p => p.startsWith("generic-trigger"));
734
-
758
+
735
759
  // Used to determine special layout for Quest controllers, e.g. last button is menu button
736
760
  this._isMetaQuestTouchController = this.profiles.includes("meta-quest-touch-plus") || this.profiles.includes("oculus-touch-v3");
737
761
 
738
762
  // Proper profile starting with v69 and browser 35.1
739
763
  this._isMxInk = this.profiles.includes("logitech-mx-ink")
740
-
764
+
741
765
  if (!this._layout) {
742
766
  // Ignore transient-pointer since we likely don't want to spawn a controller visual just for a temporary pointer.
743
767
  // TODO we should check how this is actually handled on Quest Browser when the transient-pointer flag is on.
@@ -953,7 +977,7 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
953
977
  if (menuButtonState) {
954
978
  if (menuButtonState.isDown) {
955
979
  const menu = this.context.menu;
956
- if (menu.spatialMenuIsVisible)
980
+ if (menu.spatialMenuIsVisible)
957
981
  menu.setSpatialMenuVisible(false);
958
982
  else
959
983
  this.context.menu.setSpatialMenuVisible(true);
@@ -530,7 +530,7 @@ export class NeedleXRSession implements INeedleXRSession {
530
530
  listener({ mode, init });
531
531
  }
532
532
  if (debug) showBalloonMessage("Requesting " + mode + " session (" + Date.now() + ")");
533
- this._currentSessionRequest = navigator.xr?.requestSession(mode, init);
533
+ this._currentSessionRequest = navigator?.xr?.requestSession(mode, init);
534
534
  this._currentSessionRequestMode = mode;
535
535
  /**@type {XRSystem} */
536
536
  const newSession = await (this._currentSessionRequest)?.catch(e => {
@@ -1053,10 +1053,13 @@ export class NeedleXRSession implements INeedleXRSession {
1053
1053
 
1054
1054
  /** Disconnects the controller, invokes events and notifies previou controller (if any) */
1055
1055
  private disconnectInputSource(inputSource: XRInputSource) {
1056
- const handleRemove = (oldController: NeedleXRController, _array: Array<NeedleXRController>, i: number) => {
1056
+ const handleRemove = (oldController: NeedleXRController, array: Array<NeedleXRController>) => {
1057
1057
  if (oldController.inputSource === inputSource) {
1058
1058
  if (debug) console.log("Disconnecting controller", oldController.index);
1059
- this.controllers.splice(i, 1);
1059
+
1060
+ const index = array.indexOf(oldController);
1061
+ if(index >= 0) array.splice(index, 1);
1062
+
1060
1063
  this.invokeControllerEvent(oldController, this._controllerRemoved, "removed");
1061
1064
  const args: NeedleXRControllerEventArgs = {
1062
1065
  xr: this,
@@ -1069,13 +1072,15 @@ export class NeedleXRSession implements INeedleXRSession {
1069
1072
  oldController.onDisconnected();
1070
1073
  }
1071
1074
  }
1072
- for (let i = this.controllers.length - 1; i >= 0; i--) {
1073
- const oldController = this.controllers[i];
1074
- handleRemove(oldController, this.controllers, i);
1075
+ const copy = [...this.controllers];
1076
+ for (let i = copy.length - 1; i >= 0; i--) {
1077
+ const oldController = copy[i];
1078
+ handleRemove(oldController, this.controllers);
1075
1079
  }
1076
- for (let i = this._newControllers.length - 1; i >= 0; i--) {
1077
- const oldController = this._newControllers[i];
1078
- handleRemove(oldController, this._newControllers, i);
1080
+ const copyNew = [...this._newControllers];
1081
+ for (let i = copyNew.length - 1; i >= 0; i--) {
1082
+ const oldController = copyNew[i];
1083
+ handleRemove(oldController, this._newControllers);
1079
1084
  }
1080
1085
  }
1081
1086
 
@@ -1130,8 +1135,8 @@ export class NeedleXRSession implements INeedleXRSession {
1130
1135
  for (let i = 0; i < copy.length; i++) {
1131
1136
  this.disconnectInputSource(copy[i].inputSource);
1132
1137
  }
1133
- this._newControllers.length = 0;
1134
1138
  this.controllers.length = 0;
1139
+ this._newControllers.length = 0;
1135
1140
 
1136
1141
  // we want to call leave XR for *all* scripts that are still registered
1137
1142
  // even if they might already be destroyed e.g. by the WebXR component (it destroys the default controller scripts)
@@ -1318,8 +1323,6 @@ export class NeedleXRSession implements INeedleXRSession {
1318
1323
  continue;
1319
1324
  }
1320
1325
  this.invokeCallback_ControllerAdded(script, controller);
1321
- // if (script.onXRControllerAdded)
1322
- // script.onXRControllerAdded({ xr: this, controller, change: "added" });
1323
1326
  }
1324
1327
  }
1325
1328
  this.controllers.sort((a, b) => a.index - b.index);
@@ -1558,9 +1561,10 @@ export class NeedleXRSession implements INeedleXRSession {
1558
1561
  const rigWorldScale = getWorldScale(this.rig.gameObject);
1559
1562
  minNearPlane *= rigWorldScale.x;
1560
1563
  }
1561
- if (this._camera instanceof PerspectiveCamera && this._camera.near > minNearPlane) {
1564
+ if (this._camera instanceof PerspectiveCamera && Math.abs(this._camera.near - minNearPlane) > .0001) {
1562
1565
  this.originalCameraNearPlane = this._camera.near;
1563
1566
  this._camera.near = minNearPlane;
1567
+ if(debug) console.debug(`Setting camera near plane to ${minNearPlane} (was ${this.originalCameraNearPlane}) to account for XR rendering scale`);
1564
1568
  }
1565
1569
  }
1566
1570
  }
@@ -1572,6 +1576,7 @@ export class NeedleXRSession implements INeedleXRSession {
1572
1576
 
1573
1577
  if (this._camera instanceof PerspectiveCamera && this.originalCameraNearPlane != undefined) {
1574
1578
  this._camera.near = this.originalCameraNearPlane;
1579
+ this.originalCameraNearPlane = undefined;
1575
1580
  }
1576
1581
  }
1577
1582
 
@@ -127,6 +127,7 @@ export class RendererLightmap {
127
127
  if (material["NEEDLE:lightmap-material-version"] == undefined) {
128
128
  if (debug) console.warn("Cloning material for lightmap " + material.name);
129
129
  const mat: Material = material.clone();
130
+ if(!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
130
131
  material = mat;
131
132
  material.onBeforeCompile = this.onBeforeCompile;
132
133
  }
@@ -147,7 +148,7 @@ export class RendererLightmap {
147
148
  return;
148
149
  }
149
150
 
150
- if (debug) console.log("Assigning lightmap", material.name, material.version);
151
+ if (debug) console.log("Assigning lightmap", material.name, material.version, material);
151
152
 
152
153
  // assign the lightmap
153
154
  material.lightMap = this.lightmapTexture;
@@ -55,6 +55,7 @@ export { DragMode } from "./DragControls.js";
55
55
  export type { DropListenerNetworkEventArguments, DropListenerOnDropArguments } from "./DropListener.js";
56
56
  export { type FitCameraOptions } from "./OrbitControls.js";
57
57
  export * from "./particlesystem/api.js"
58
+ export * from "./splines/index.js";
58
59
 
59
60
  // for correct type resolution in JSDoc
60
61
  import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
@@ -150,6 +150,9 @@ export { SmoothFollow } from "../SmoothFollow.js";
150
150
  export { SpatialTriggerReceiver } from "../SpatialTrigger.js";
151
151
  export { SpatialTrigger } from "../SpatialTrigger.js";
152
152
  export { SpectatorCamera } from "../SpectatorCamera.js";
153
+ export { SplineData } from "../splines/Spline.js";
154
+ export { SplineContainer } from "../splines/Spline.js";
155
+ export { SplineWalker } from "../splines/SplineWalker.js";
153
156
  export { Sprite } from "../SpriteRenderer.js";
154
157
  export { SpriteSheet } from "../SpriteRenderer.js";
155
158
  export { SpriteData } from "../SpriteRenderer.js";
@@ -0,0 +1,284 @@
1
+
2
+ import { BufferGeometry, CatmullRomCurve3, CubicBezierCurve3, Curve, Line, LineBasicMaterial,LineCurve3, Object3D, Quaternion, Vector3 } from "three";
3
+
4
+ import { Mathf } from "../../engine/engine_math.js";
5
+ import { serializeable } from "../../engine/engine_serialization.js";
6
+ import { getParam } from "../../engine/engine_utils.js";
7
+ import { Behaviour } from "../Component.js";
8
+
9
+ const debug = getParam("debugsplines");
10
+
11
+ export class SplineData {
12
+ @serializeable(Vector3)
13
+ position: Vector3 = new Vector3();
14
+
15
+ @serializeable(Quaternion)
16
+ rotation: Quaternion = new Quaternion();
17
+
18
+ @serializeable(Vector3)
19
+ tangentIn: Vector3 = new Vector3();
20
+
21
+ @serializeable(Vector3)
22
+ tangentOut: Vector3 = new Vector3();
23
+ }
24
+
25
+ // enum SplineTypeEnum {
26
+ // CatmullRom = 0,
27
+ // Bezier = 1,
28
+ // Linear = 2
29
+ // }
30
+ // type SplineType = "CatmullRom" | "Bezier" | "Linear";
31
+
32
+
33
+ //@dont-generate-component
34
+ /**
35
+ * Holds spline data and generates a spline curve. Use with SplineWalker to move objects along the spline or call getPointAt to sample points on the spline.
36
+ * The spline is defined by an array of knots (SplineData) which define position, rotation and tangents.
37
+ *
38
+ * You can create a SplineContainer from an array of points using the static method 'createFromPoints'.
39
+ */
40
+ export class SplineContainer extends Behaviour {
41
+
42
+ /**
43
+ * Adds a knot to the end of the spline.
44
+ */
45
+ addKnot(knot: SplineData | { position: Vector3 }): SplineContainer {
46
+ if (knot instanceof SplineData) {
47
+ this.spline.push(knot);
48
+ this._isDirty = true;
49
+ }
50
+ else {
51
+ const k = new SplineData();
52
+ k.position.copy(knot.position);
53
+ this.spline.push(k);
54
+ this._isDirty = true;
55
+ }
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Removes a knot by index or by reference.
61
+ */
62
+ removeKnot(index: number | SplineData): SplineContainer {
63
+ if (typeof index === "number") {
64
+ this.spline.splice(index, 1);
65
+ this._isDirty = true;
66
+ } else {
67
+ const i = this.spline.indexOf(index);
68
+ if (i !== -1) {
69
+ this.spline.splice(i, 1);
70
+ this._isDirty = true;
71
+ }
72
+ }
73
+ return this;
74
+ }
75
+
76
+ /**
77
+ * Gets a point on the spline in world space.
78
+ */
79
+ getPointAt(t: number, target?: Vector3): Vector3 {
80
+ if (!this.curve) return new Vector3();
81
+ const pos = this.curve.getPointAt(Mathf.clamp01(t), target);
82
+ const worldMatrix = this.gameObject.matrixWorld ?? undefined;
83
+ if (worldMatrix) {
84
+ pos.applyMatrix4(worldMatrix);
85
+ }
86
+ return pos;
87
+ }
88
+
89
+ /**
90
+ * Marks the spline as dirty, causing it to be rebuilt on the next update.
91
+ */
92
+ markDirty() {
93
+ this._isDirty = true;
94
+ }
95
+
96
+ /**
97
+ * Gets the tangent vector on the spline in world space.
98
+ */
99
+ getTangentAt(t: number, target?: Vector3): Vector3 {
100
+ if (!this.curve) return target ?? new Vector3();
101
+ const wr = this.gameObject.worldQuaternion;
102
+ return this.curve.getTangentAt(Mathf.clamp01(t), target).applyQuaternion(wr);
103
+ }
104
+
105
+ @serializeable()
106
+ set closed(value: boolean) {
107
+ this._closed = value;
108
+ this._isDirty = true;
109
+ }
110
+ get closed() { return this._closed; }
111
+ private _closed: boolean = false;
112
+
113
+
114
+ /** Spline data. Call 'markDirty' if modified */
115
+ @serializeable(SplineData)
116
+ spline: SplineData[] = [];
117
+
118
+ /** Enable to render the spline curve for debugging */
119
+ set debug(debug: boolean) {
120
+ if (debug && !this._builtCurve) this.buildCurve();
121
+ if (!this._debugLine) return;
122
+ this._debugLine.visible = debug;
123
+ }
124
+
125
+ /** Gets the spline curve generated from the 'spline' data */
126
+ get curve(): Curve<Vector3> | null {
127
+ return this._curve;
128
+ }
129
+
130
+ get isDirty() { return this._isDirty; }
131
+
132
+ private _isDirty: boolean = false;
133
+
134
+ private _curve: Curve<Vector3> | null = null;
135
+ private _builtCurve: boolean = false;
136
+ private _debugLine: Object3D | null = null;
137
+
138
+ /** @internal */
139
+ awake() {
140
+ if (debug) {
141
+ console.log(`[Spline] ${this.name}`, this);
142
+ this.buildCurve();
143
+ }
144
+ }
145
+
146
+ /** @internal */
147
+ update() {
148
+ if (this._isDirty) {
149
+ this.buildCurve(true);
150
+ }
151
+ if (this._debugLine && this._debugLine.parent !== this.gameObject) this.gameObject.add(this._debugLine);
152
+ }
153
+
154
+ private buildCurve(force: boolean = false) {
155
+ if (this._builtCurve && !force) return;
156
+ this._builtCurve = true;
157
+
158
+ if (!this.spline) {
159
+ console.error("[Spline] Can not build curve, no spline data", this.name);
160
+ return;
161
+ }
162
+ this._isDirty = false;
163
+ this._curve = createCatmullRomCurve(this.spline, this.closed);
164
+ this.buildDebugCurve();
165
+ // TODO: Unity supports spline interpolation type per knot which we don't support right now. Additionally EditType is deprecated. For simplicity we're just supporting CatmullRom for now.
166
+ // switch (this.editType) {
167
+ // case SplineType.CatmullRom:
168
+ // this.createCatmullRomCurve();
169
+ // break;
170
+ // case SplineType.Bezier:
171
+ // console.warn("Bezier spline not implemented yet", this.name);
172
+ // this.createCatmullRomCurve();
173
+ // // this.createBezierCurve();
174
+ // break;
175
+ // case SplineType.Linear:
176
+ // this.createLinearCurve();
177
+ // break;
178
+ // }
179
+ }
180
+
181
+ private buildDebugCurve() {
182
+ if (debug && this.spline && this._curve) {
183
+ this._debugLine?.removeFromParent();
184
+ this._debugLine = null;
185
+
186
+ const material = new LineBasicMaterial({
187
+ color: 0x6600ff,
188
+ });
189
+ const res = this.spline.length * 10;
190
+ const splinePoints = this._curve.getPoints(res);
191
+ const geometry = new BufferGeometry().setFromPoints(splinePoints);
192
+ this._debugLine = new Line(geometry, material);
193
+ this.gameObject?.add(this._debugLine);
194
+ }
195
+ }
196
+ }
197
+
198
+
199
+ function createCatmullRomCurve(data: SplineData[], closed: boolean): CatmullRomCurve3 {
200
+ const points = data.map(knot => new Vector3(-knot.position.x, knot.position.y, knot.position.z));
201
+ if (points.length === 1) points.push(points[0]);
202
+ const averageTension = data.reduce((acc, knot) => acc + Math.abs(knot.tangentOut.x) + Math.abs(knot.tangentOut.y) + Math.abs(knot.tangentOut.z), 0) / data.length;
203
+ const tension = Mathf.remap(averageTension, 0, 0.3, 0, .5);
204
+ return new CatmullRomCurve3(points, closed, "catmullrom", tension);
205
+ }
206
+
207
+ function createLinearCurve(data: SplineData[], closed: boolean): LineCurve3 | null {
208
+ if (!data || data.length < 2) return null;
209
+ const points = data.map(knot => new Vector3(-knot.position.x, knot.position.y, knot.position.z));
210
+ if (closed) points.push(points[0]);
211
+ return new LineCurve3(points.at(0), points.at(1));
212
+ }
213
+
214
+
215
+ // function createBezierCurve(data: SplineData[], closed: boolean): CubicBezierCurve3 | null {
216
+ // if (!data || data.length < 2) return null;
217
+
218
+ // for (let k = 0; k < data.length; k++) {
219
+ // const k0 = data[k];
220
+ // let nextIndex = k + 1;
221
+ // if (nextIndex >= data.length) {
222
+ // if (!closed) break;
223
+ // nextIndex = 0;
224
+ // }
225
+ // const k1 = data[nextIndex];
226
+ // // points
227
+ // const p0 = new Vector3(-k0.position.x, k0.position.y, k0.position.z);
228
+ // const p1 = new Vector3(-k1.position.x, k1.position.y, k1.position.z);
229
+ // // tangents
230
+ // const t0 = new Vector3(-k0.tangentOut.x, k0.tangentOut.y, k0.tangentOut.z);
231
+ // const t1 = new Vector3(-k1.tangentIn.x, k1.tangentIn.y, k1.tangentIn.z);
232
+ // // rotations
233
+ // // const q0 = k0.rotation;// new Quaternion(k0.rotation.value.x, k0.rotation.value.y, k0.rotation.value.z, k0.rotation.value.w);
234
+ // // const q1 = k1.rotation;// new Quaternion(k1.rotation.value.x, k1.rotation.value.y, k1.rotation.value.z, k1.rotation.value.w);
235
+ // // const a = new Vector3(0,1,0);
236
+ // // const angle = Math.PI*.5;
237
+ // // t0.sub(p0).applyQuaternion(q0).add(p0);
238
+ // // t1.sub(p1).applyQuaternion(q1).add(p1);
239
+ // t0.add(p0);
240
+ // // t0.applyQuaternion(q0);
241
+ // t1.add(p1);
242
+ // const curve = new CubicBezierCurve3(p0, t0, t1, p1);
243
+ // return curve;
244
+ // }
245
+ // return null;
246
+ // }
247
+
248
+ // class SplineCurve {
249
+
250
+ // private spline: Spline;
251
+
252
+ // constructor(spline: Spline) {
253
+ // this.spline = spline;
254
+ // }
255
+
256
+ // getPoints(num: number): Vector3[] {
257
+ // const points: Vector3[] = [];
258
+ // const samplePerKnot = num / this.spline.length;
259
+ // for (let k = 1; k < this.spline.length; k++) {
260
+ // const cur = this.spline[k];
261
+ // const prev = this.spline[k - 1];
262
+
263
+ // for (let i = 0; i < samplePerKnot; i++) {
264
+ // const t = i / (samplePerKnot - 1);
265
+ // console.log(CurveUtils);
266
+ // const x = this.interpolate(-prev.Position.x, -cur.Position.x, -prev.tangentOut.x, -cur.TangentIn.x, t);
267
+ // const y = this.interpolate(prev.Position.y, cur.Position.y, prev.tangentOut.y, cur.TangentIn.y, t);
268
+ // const z = this.interpolate(prev.Position.z, cur.Position.z, prev.tangentOut.z, cur.TangentIn.z, t);
269
+ // points.push(new Vector3(x, y, z));
270
+ // }
271
+ // }
272
+
273
+ // return points;
274
+ // }
275
+
276
+ // interpolate(p0, p1, p2, p3, t) {
277
+
278
+ // var v0 = (p2 - p0) * 0.5;
279
+ // var v1 = (p3 - p1) * 0.5;
280
+ // var t2 = t * t;
281
+ // var t3 = t * t2;
282
+ // return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (- 3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
283
+ // }
284
+ // }
@@ -0,0 +1,32 @@
1
+ import { Vector3 } from "three";
2
+
3
+ import { Mathf } from "../../engine/engine_math.js";
4
+ import { SplineContainer, SplineData } from "./Spline.js";
5
+
6
+
7
+ export namespace SplineUtils {
8
+
9
+ /**
10
+ * Creates a SplineContainer from an array of points.
11
+ * @param positions The positions of the knots.
12
+ * @param closed Whether the spline is closed (the last knot connects to the first).
13
+ * @param tension The tension of the spline. 0 is no tension, 1 is high tension (straight lines between knots). Default is 0.75.
14
+ * @return The created SplineContainer component - add it to an Object3D to use it.
15
+ */
16
+ export function createFromPoints(positions: Vector3[], closed: boolean = false, tension: number = .75): SplineContainer {
17
+ const spline = new SplineContainer();
18
+ const tangentFactor = 1 - Mathf.clamp(tension, 0, 1);
19
+ positions.forEach((pos, index) => {
20
+ const tangent = new Vector3();
21
+ if (index < positions.length - 1) tangent.subVectors(positions[index + 1], pos).normalize().multiplyScalar(tangentFactor);
22
+ else if (closed && positions.length > 1) tangent.subVectors(positions[0], pos).normalize().multiplyScalar(tangentFactor);
23
+ const knot = new SplineData();
24
+ knot.position.copy(pos);
25
+ knot.tangentIn.copy(tangent);
26
+ knot.tangentOut.copy(tangent);
27
+ spline.addKnot(knot);
28
+ });
29
+ spline.closed = closed;
30
+ return spline;
31
+ }
32
+ }