Needle Engine

Changes between version 4.2.2 and 4.2.3
Files changed (8) hide show
  1. plugins/vite/pwa.js +33 -22
  2. src/engine/engine_context.ts +7 -3
  3. src/engine/engine_loaders.ts +6 -10
  4. src/engine/engine_physics_rapier.ts +1 -1
  5. src/engine-components/ui/InputField.ts +9 -0
  6. src/engine-components/OrbitControls.ts +71 -21
  7. src/engine/codegen/register_types.ts +2 -2
  8. plugins/types/userconfig.d.ts +2 -2
plugins/vite/pwa.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { getOutputDirectory } from './config.js';
5
5
  import { getPosterPath } from './poster.js';
6
6
 
7
+ const pwaErrorWithInstructions = "It seems that you're trying to build a PWA using 'vite-plugin-pwa'!\nNeedle can manage PWA settings for you – just pass the same 'pwaOptions' to the needlePlugins and VitePWA plugins:\n\n1. Install the vite PWA plugin: npm install vite-plugin-pwa --save-dev\n\n2. Then update your vite.config.js:\n\n import { VitePWA } from 'vite-plugin-pwa';\n ...\n needlePlugins(command, needleConfig, { pwa: pwaOptions }),\n VitePWA(pwaOptions),\n\nIf you want to manage PWA building yourself and skip this check, please pass '{ pwa: false }' to needlePlugins.";
7
8
 
8
9
  /** Provides reasonable defaults for a PWA manifest and workbox settings.
9
10
  * @param {import('../types').userSettings} userSettings
@@ -13,7 +14,9 @@
13
14
  export const needlePWA = (command, config, userSettings) => {
14
15
  // @ts-ignore // TODO correctly type the userSettings.pwaOptions object
15
16
  /** @type {import("vite-plugin-pwa").VitePWAOptions | false} */
16
- const pwaOptions = userSettings.pwa;
17
+ const pwaOptions = userSettings.pwa === true
18
+ ? { } // allow setting `pwa: true`
19
+ : userSettings.pwa;
17
20
 
18
21
  /** The context contains files that are generated by the plugin and should be deleted after
19
22
  * @type {import('../types').NeedlePWAProcessContext} */
@@ -29,7 +32,7 @@
29
32
  apply: "build",
30
33
  configResolved(viteConfig) {
31
34
  if (findVitePWAPlugin(viteConfig)) {
32
- errorThrow("It seems that you're trying to build a PWA using `vite-plugin-pwa`!\nNeedle can manage PWA settings for you – just pass the same `pwaOptions` to the `needlePlugin` and `VitePWA` plugins:\n\n import { VitePWA } from 'vite-plugin-pwa';\n ...\n needlePlugins(command, needleConfig, { pwa: pwaOptions }),\n VitePWA(pwaOptions),\n\nIf you want to manage PWA building yourself and skip this check, please pass `{ pwa: false }` to needlePlugins.");
35
+ errorThrow(pwaErrorWithInstructions);
33
36
  }
34
37
  },
35
38
  }
@@ -88,7 +91,7 @@
88
91
  let pwaPluginIndex = -1;
89
92
  let gzipPlugin = null;
90
93
  if (viteConfig.plugins) {
91
- for (let i = viteConfig.plugins.length-1; i >= 0; i--) {
94
+ for (let i = viteConfig.plugins.length - 1; i >= 0; i--) {
92
95
  const plugin = viteConfig.plugins[i];
93
96
  if (plugin && "name" in plugin && plugin.name === "vite:compression") {
94
97
  gzipPluginIndex = i;
@@ -152,7 +155,7 @@
152
155
  try {
153
156
  const plugin = findVitePWAPlugin(config);
154
157
  if (!plugin) {
155
- errorThrow("It seems that you're trying to build a PWA!.\nRun `npm install vite-plugin-pwa --save-dev` to install the plugin\nThen add VitePWA to your vite.config.js and pass the pwaOptions to both Needle and VitePWA:\n\n import { VitePWA } from 'vite-plugin-pwa';\n \n const pwaOptions = {};\n \n plugins: [\n needlePlugins(command, needleConfig, { pwa: pwaOptions }),\n VitePWA(pwaOptions),\n ]\n\nIf you don't intend to build a PWA, pass `{ pwa: false }` to needlePlugins or remove the `pwa` entry.");
158
+ errorThrow(pwaErrorWithInstructions);
156
159
  }
157
160
 
158
161
  // check if the index header contains the webmanifest ALSO
@@ -547,28 +550,36 @@
547
550
  ],
548
551
  runtimeCaching: [
549
552
  // allow caching Google Fonts
550
- {...externalResourceCaching, ...{
551
- urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
552
- options: { cacheName: 'google-fonts-cache' },
553
- }},
553
+ {
554
+ ...externalResourceCaching, ...{
555
+ urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
556
+ options: { cacheName: 'google-fonts-cache' },
557
+ }
558
+ },
554
559
  // allow caching static resources from Google, like CSS
555
- {...externalResourceCaching, ...{
556
- urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
557
- options: { cacheName: 'gstatic-fonts-cache' },
558
- }},
560
+ {
561
+ ...externalResourceCaching, ...{
562
+ urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
563
+ options: { cacheName: 'gstatic-fonts-cache' },
564
+ }
565
+ },
559
566
  // allow caching Needle cdn resources
560
- {...externalResourceCaching, ...{
561
- urlPattern: /^https:\/\/cdn\.needle\.tools\/.*/i,
562
- handler: 'NetworkFirst',
563
- options: { cacheName: 'needle-cdn-cache' },
564
- }},
567
+ {
568
+ ...externalResourceCaching, ...{
569
+ urlPattern: /^https:\/\/cdn\.needle\.tools\/.*/i,
570
+ handler: 'NetworkFirst',
571
+ options: { cacheName: 'needle-cdn-cache' },
572
+ }
573
+ },
565
574
  // allow caching controller resources,
566
575
  // https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/
567
- {...externalResourceCaching, ...{
568
- urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/npm\/@webxr-input-profiles\/assets@1\.0\/dist\/profiles\/.*/i,
569
- handler: 'NetworkFirst',
570
- options: { cacheName: 'webxr-controller-cache' },
571
- }},
576
+ {
577
+ ...externalResourceCaching, ...{
578
+ urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/npm\/@webxr-input-profiles\/assets@1\.0\/dist\/profiles\/.*/i,
579
+ handler: 'NetworkFirst',
580
+ options: { cacheName: 'webxr-controller-cache' },
581
+ }
582
+ },
572
583
  // allow caching local resources
573
584
  {
574
585
  urlPattern: ({ url }) => url,
src/engine/engine_context.ts CHANGED
@@ -1468,10 +1468,14 @@
1468
1468
  return true;
1469
1469
  }
1470
1470
 
1471
+ private _contextRestoreTries = 0;
1471
1472
  private handleRendererContextLost() {
1472
- if (this.renderer.getContext().isContextLost()) {
1473
- console.warn("Attempting to recover WebGL context...");
1474
- this.renderer.forceContextRestore();
1473
+ // Try to restore the context every x frames
1474
+ if (this.time.frame % 10 && this.renderer.getContext().isContextLost()) {
1475
+ if (this._contextRestoreTries++ < 100) {
1476
+ console.warn("Attempting to recover WebGL context...");
1477
+ this.renderer.forceContextRestore();
1478
+ }
1475
1479
  }
1476
1480
  }
1477
1481
 
src/engine/engine_loaders.ts CHANGED
@@ -23,26 +23,22 @@
23
23
  export function setDracoDecoderPath(path: string | undefined) {
24
24
  if (path !== undefined && typeof path === "string") {
25
25
  setDracoDecoderLocation(path);
26
- const loaders = ensureLoaders();
27
- if (debug) console.log("Setting draco decoder path to", path);
28
- loaders.dracoLoader.setDecoderPath(path);
29
26
  }
30
27
  }
31
28
 
32
29
  export function setDracoDecoderType(type: string | undefined) {
33
30
  if (type !== undefined && typeof type === "string") {
34
- const loaders = ensureLoaders();
35
- if (debug) console.log("Setting draco decoder type to", type);
36
- loaders.dracoLoader.setDecoderConfig({ type: type });
31
+ if (type !== "js") {
32
+ const loaders = ensureLoaders();
33
+ if (debug) console.log("Setting draco decoder type to", type);
34
+ loaders.dracoLoader.setDecoderConfig({ type: type });
35
+ }
37
36
  }
38
37
  }
39
38
 
40
39
  export function setKtx2TranscoderPath(path: string) {
41
40
  if (path !== undefined && typeof path === "string") {
42
41
  setKTX2TranscoderLocation(path);
43
- const loaders = ensureLoaders();
44
- if (debug) console.log("Setting ktx2 transcoder path to", path);
45
- loaders.ktx2Loader.setTranscoderPath(path);
46
42
  }
47
43
  }
48
44
 
@@ -78,7 +74,7 @@
78
74
  if (!(loader as any).meshoptDecoder)
79
75
  loader.setMeshoptDecoder(loaders.meshoptDecoder);
80
76
 
81
- configureLoader(loader, {
77
+ configureLoader(loader, {
82
78
  progressive: true,
83
79
  });
84
80
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -710,7 +710,7 @@
710
710
  positions = this._meshCache.get(key)!;
711
711
  }
712
712
  else {
713
- if (debugPhysics || isDevEnvironment()) console.warn(`Your MeshCollider \"${collider.name}\" is scaled: consider applying the scale to the collider mesh instead (${scale.x}, ${scale.y}, ${scale.z})`);
713
+ if (debugPhysics || isDevEnvironment()) console.debug(`[Performance] Your MeshCollider \"${collider.name}\" is scaled: consider applying the scale to the collider mesh instead (${scale.x}, ${scale.y}, ${scale.z})`);
714
714
  const scaledPositions = new Float32Array(positions.length);
715
715
  for (let i = 0; i < positions.length; i += 3) {
716
716
  scaledPositions[i] = positions[i] * scale.x;
src/engine-components/ui/InputField.ts CHANGED
@@ -18,6 +18,15 @@
18
18
  get text(): string {
19
19
  return this.textComponent?.text ?? "";
20
20
  }
21
+ set text(value: string) {
22
+ if (this.textComponent) {
23
+ this.textComponent.text = value;
24
+ if (this.placeholder) {
25
+ if (value.length > 0) this.placeholder.gameObject.visible = false;
26
+ else this.placeholder.gameObject.visible = true;
27
+ }
28
+ }
29
+ }
21
30
 
22
31
  get isFocused() {
23
32
  return InputField.active === this;
src/engine-components/OrbitControls.ts CHANGED
@@ -1,13 +1,14 @@
1
- import { Box3Helper, Object3D, PerspectiveCamera, Ray, Vector2, Vector3, Vector3Like } from "three";
1
+ import { Box3Helper, Euler, Object3D, PerspectiveCamera, Quaternion, Ray, Vector2, Vector3, Vector3Like } from "three";
2
2
  import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
3
3
 
4
+ import { isDevEnvironment } from "../engine/debug/index.js";
4
5
  import { setCameraController } from "../engine/engine_camera.js";
5
6
  import { Gizmos } from "../engine/engine_gizmos.js";
6
7
  import { InputEventQueue, NEPointerEvent } from "../engine/engine_input.js";
7
8
  import { Mathf } from "../engine/engine_math.js";
8
9
  import { RaycastOptions } from "../engine/engine_physics.js";
9
10
  import { serializable } from "../engine/engine_serialization_decorator.js";
10
- import { getBoundingBox, getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
11
+ import { getBoundingBox, getTempVector, getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
11
12
  import type { ICameraController } from "../engine/engine_types.js";
12
13
  import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
13
14
  import { Camera } from "./Camera.js";
@@ -616,14 +617,38 @@
616
617
 
617
618
 
618
619
  /**
619
- * Sets camera target position and look direction. Does perform a raycast in the forward direction of the passed in object to find an orbit point
620
+ * Sets camera target position and look direction using a raycast in forward direction of the object.
621
+ *
622
+ * @param source The object to raycast from. If a camera is passed in the camera position will be used as the source.
623
+ * @param immediateOrDuration If true the camera target will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
624
+ *
625
+ * This is useful for example if you want to align your camera with an object in your scene (or another camera). Simply pass in this other camera object
626
+ * @returns true if the target was set successfully
620
627
  */
621
- public setCameraAndLookTarget(target: Object3D) {
622
- if (!target || !(target instanceof Object3D)) return;
623
- const worldPosition = getWorldPosition(target);
624
- const forward = getWorldDirection(target);
625
- this.setTargetFromRaycast(new Ray(worldPosition, forward));
626
- this.setCameraTargetPosition(worldPosition);
628
+ public setCameraAndLookTarget(source: Object3D | Camera, immediateOrDuration: number | boolean = false): boolean {
629
+ if (!source) {
630
+ if (isDevEnvironment()) console.warn("[OrbitControls] setCameraAndLookTarget target is null");
631
+ return false;
632
+ }
633
+ if (!(source instanceof Object3D) && !(source instanceof Camera)) {
634
+ if (isDevEnvironment()) console.warn("[OrbitControls] setCameraAndLookTarget target is not an Object3D or Camera");
635
+ return false;
636
+ }
637
+ if (source instanceof Camera) {
638
+ source = source.gameObject;
639
+ }
640
+ const worldPosition = source.worldPosition;
641
+ const forward = source.worldForward;
642
+ const ray = new Ray(worldPosition, forward.multiplyScalar(-1));
643
+
644
+ if (debug) Gizmos.DrawRay(ray.origin, ray.direction, 0xff0000, 10);
645
+
646
+ if (!this.setTargetFromRaycast(ray, immediateOrDuration)) {
647
+ this.setLookTargetPosition(ray.at(2, getTempVector()), immediateOrDuration);
648
+ }
649
+
650
+ this.setCameraTargetPosition(worldPosition, immediateOrDuration);
651
+ return true;
627
652
  }
628
653
 
629
654
  /** Moves the camera to position smoothly.
@@ -653,6 +678,38 @@
653
678
  else this._cameraLerpDuration = this.targetLerpDuration;
654
679
  }
655
680
  }
681
+ // public setCameraTargetRotation(rotation: Vector3 | Euler | Quaternion, immediateOrDuration: boolean | number = false): void {
682
+ // if (!this._cameraObject) return;
683
+
684
+ // if (typeof immediateOrDuration === "boolean") immediateOrDuration = immediateOrDuration ? 0 : this.targetLerpDuration;
685
+
686
+ // const ray = new Ray(this._cameraObject.worldPosition, getTempVector(0, 0, 1));
687
+
688
+ // // if the camera is in the middle of lerping we use the end position for the raycast
689
+ // if (immediateOrDuration > 0 && this._cameraEndPosition && this._cameraLerpActive) {
690
+ // ray.origin = getTempVector(this._cameraEndPosition)
691
+ // }
692
+
693
+ // if (rotation instanceof Vector3) {
694
+ // rotation = new Euler().setFromVector3(rotation);
695
+ // }
696
+ // if (rotation instanceof Euler) {
697
+ // rotation = new Quaternion().setFromEuler(rotation);
698
+ // }
699
+
700
+ // ray.direction.applyQuaternion(rotation);
701
+ // ray.direction.multiplyScalar(-1);
702
+
703
+ // const hits = this.context.physics.raycastFromRay(ray);
704
+
705
+ // if (hits.length > 0) {
706
+ // this.setCameraTargetPosition(hits[0].point, immediateOrDuration);
707
+ // }
708
+ // else {
709
+ // this.setLookTargetPosition(ray.at(2, getTempVector()));
710
+ // }
711
+ // }
712
+
656
713
  /** True while the camera position is being lerped */
657
714
  get cameraLerpActive() { return this._cameraLerpActive; }
658
715
  /** Call to stop camera position lerping */
@@ -747,8 +804,8 @@
747
804
  else this._controls.target.lerp(position, delta);
748
805
  }
749
806
 
750
- private setTargetFromRaycast(ray?: Ray) {
751
- if (!this.controls) return;
807
+ private setTargetFromRaycast(ray?: Ray, immediateOrDuration: number | boolean = false): boolean {
808
+ if (!this.controls) return false;
752
809
  const rc = ray ? this.context.physics.raycastFromRay(ray) : this.context.physics.raycast();
753
810
  for (const hit of rc) {
754
811
  if (hit.distance > 0 && GameObject.isActiveInHierarchy(hit.object)) {
@@ -760,18 +817,11 @@
760
817
  break;
761
818
  }
762
819
  }
763
-
764
- this.setLookTargetPosition(hit.point);
765
-
766
- // if (this.context.mainCamera) {
767
- // const pos = getWorldPosition(this.context.mainCamera);
768
- // const cameraTarget = pos.clone().sub(this.controls.target).add(this._lookTargetEndPosition);
769
- // this._cameraObject?.parent?.worldToLocal(cameraTarget);
770
- // this.setCameraTargetPosition(cameraTarget);
771
- // }
772
- break;
820
+ this.setLookTargetPosition(hit.point, immediateOrDuration);
821
+ return true;
773
822
  }
774
823
  }
824
+ return false;
775
825
  }
776
826
 
777
827
  // Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
src/engine/codegen/register_types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable */
2
2
  import { TypeStore } from "./../engine_typestore.js"
3
-
3
+
4
4
  // Import types
5
5
  import { AlignmentConstraint } from "../../engine-components/AlignmentConstraint.js";
6
6
  import { Animation } from "../../engine-components/Animation.js";
@@ -220,7 +220,7 @@
220
220
  import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
221
221
  import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync.js";
222
222
  import { PresentationMode } from "../../engine-components-experimental/Presentation.js";
223
-
223
+
224
224
  // Register types
225
225
  TypeStore.add("AlignmentConstraint", AlignmentConstraint);
226
226
  TypeStore.add("Animation", Animation);
plugins/types/userconfig.d.ts CHANGED
@@ -85,8 +85,8 @@
85
85
  */
86
86
  posterGenerationMode?: "default" | "once";
87
87
 
88
- /** Pass in a mix of VitePWA and NeedlePWA options, or "false" */
89
- /** @type {import("vite-plugin-pwa").VitePWAOptions & Partial<NeedlePWAOptions> | false} */
88
+ /** Pass in a mix of VitePWA and NeedlePWA options, true to enable with defaults or "false" to disable */
89
+ /** @type {import("vite-plugin-pwa").VitePWAOptions & Partial<NeedlePWAOptions> | boolean} */
90
90
  pwa?: undefined;
91
91
 
92
92
  /** used by nextjs config to forward the webpack module */