@needle-tools/engine 4.2.2 → 4.2.3

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 (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/gltf-progressive.js +258 -257
  3. package/dist/gltf-progressive.light.js +258 -257
  4. package/dist/gltf-progressive.light.min.js +7 -7
  5. package/dist/gltf-progressive.light.umd.cjs +7 -7
  6. package/dist/gltf-progressive.min.js +7 -7
  7. package/dist/gltf-progressive.umd.cjs +7 -7
  8. package/dist/needle-engine.bundle.js +3455 -3426
  9. package/dist/needle-engine.bundle.light.js +3449 -3420
  10. package/dist/needle-engine.bundle.light.min.js +106 -106
  11. package/dist/needle-engine.bundle.light.umd.cjs +103 -103
  12. package/dist/needle-engine.bundle.min.js +106 -106
  13. package/dist/needle-engine.bundle.umd.cjs +101 -101
  14. package/dist/needle-engine.d.ts +9 -9
  15. package/dist/needle-engine.light.d.ts +9 -9
  16. package/lib/engine/engine_context.d.ts +1 -0
  17. package/lib/engine/engine_context.js +7 -3
  18. package/lib/engine/engine_context.js.map +1 -1
  19. package/lib/engine/engine_loaders.js +6 -12
  20. package/lib/engine/engine_loaders.js.map +1 -1
  21. package/lib/engine/engine_physics_rapier.js +1 -1
  22. package/lib/engine/engine_physics_rapier.js.map +1 -1
  23. package/lib/engine-components/OrbitControls.d.ts +9 -2
  24. package/lib/engine-components/OrbitControls.js +62 -19
  25. package/lib/engine-components/OrbitControls.js.map +1 -1
  26. package/lib/engine-components/ui/InputField.d.ts +1 -0
  27. package/lib/engine-components/ui/InputField.js +11 -0
  28. package/lib/engine-components/ui/InputField.js.map +1 -1
  29. package/package.json +2 -2
  30. package/plugins/types/userconfig.d.ts +2 -2
  31. package/plugins/vite/pwa.js +33 -22
  32. package/src/engine/codegen/register_types.ts +2 -2
  33. package/src/engine/engine_context.ts +7 -3
  34. package/src/engine/engine_loaders.ts +6 -10
  35. package/src/engine/engine_physics_rapier.ts +1 -1
  36. package/src/engine-components/OrbitControls.ts +71 -21
  37. package/src/engine-components/ui/InputField.ts +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "4.2.2",
3
+ "version": "4.2.3",
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": {
@@ -86,7 +86,7 @@
86
86
  ],
87
87
  "dependencies": {
88
88
  "@dimforge/rapier3d-compat": "^0.14.0",
89
- "@needle-tools/gltf-progressive": "^2.1.0",
89
+ "@needle-tools/gltf-progressive": "^2.1.1",
90
90
  "@webxr-input-profiles/motion-controllers": "^1.0.0",
91
91
  "flatbuffers": "2.0.4",
92
92
  "md5": "^2.3.0",
@@ -85,8 +85,8 @@ export type userSettings = {
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 */
@@ -4,6 +4,7 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync,
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 @@ import { getPosterPath } from './poster.js';
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 @@ export const needlePWA = (command, config, userSettings) => {
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 @@ export const needlePWA = (command, config, userSettings) => {
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 @@ export const needlePWA = (command, config, userSettings) => {
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 @@ function processWorkboxConfig(manifest) {
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,
@@ -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 @@ import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
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);
@@ -1468,10 +1468,14 @@ export class Context implements IContext {
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
 
@@ -23,26 +23,22 @@ function ensureLoaders() {
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 @@ export function addDracoAndKTX2Loaders(loader: GLTFLoader, context: Pick<Context
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
 
@@ -710,7 +710,7 @@ export class RapierPhysics implements IPhysicsEngine {
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;
@@ -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 @@ export class OrbitControls extends Behaviour implements ICameraController {
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 @@ export class OrbitControls extends Behaviour implements ICameraController {
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 @@ export class OrbitControls extends Behaviour implements ICameraController {
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 @@ export class OrbitControls extends Behaviour implements ICameraController {
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
@@ -18,6 +18,15 @@ export class InputField extends Behaviour implements IPointerEventHandler {
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;