@needle-tools/engine 4.9.3 → 4.10.0-beta.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 (133) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-DhE1A6hX.min.js → gltf-progressive-CoZbSfPR.min.js} +1 -1
  4. package/dist/{gltf-progressive-egsMzRdv.js → gltf-progressive-DUR9TuAH.js} +3 -3
  5. package/dist/{gltf-progressive-DWiyqrwB.umd.cjs → gltf-progressive-Iy7aSAPk.umd.cjs} +1 -1
  6. package/dist/{needle-engine.bundle-C7LSzO5L.umd.cjs → needle-engine.bundle-6so_os_w.umd.cjs} +179 -145
  7. package/dist/{needle-engine.bundle-BAsxNKpA.js → needle-engine.bundle-Dj2DYdMY.js} +7699 -7235
  8. package/dist/needle-engine.bundle-Djy6H4lx.min.js +1650 -0
  9. package/dist/needle-engine.js +460 -456
  10. package/dist/needle-engine.min.js +1 -1
  11. package/dist/needle-engine.umd.cjs +1 -1
  12. package/dist/{postprocessing-BZOSD1ln.min.js → postprocessing-BHMVuZQ1.min.js} +1 -1
  13. package/dist/{postprocessing-Bb5StX0o.umd.cjs → postprocessing-BsnRNRRS.umd.cjs} +1 -1
  14. package/dist/{postprocessing-BzFF7i-7.js → postprocessing-DQ2pynXW.js} +2 -2
  15. package/dist/{three-BK56xWDs.umd.cjs → three-B-jwTHao.umd.cjs} +11 -11
  16. package/dist/{three-CsHK73Zc.js → three-CJSAehtG.js} +1 -0
  17. package/dist/{three-examples-Bph291U2.min.js → three-examples-BivkhnvN.min.js} +1 -1
  18. package/dist/{three-examples-C9WfZu-X.umd.cjs → three-examples-Deqc1bNw.umd.cjs} +1 -1
  19. package/dist/{three-examples-BvMpKSun.js → three-examples-Doq0rvFU.js} +1 -1
  20. package/dist/{three-mesh-ui-CN6aRT7i.js → three-mesh-ui-CktOi6oI.js} +1 -1
  21. package/dist/{three-mesh-ui-DnxkZWNA.umd.cjs → three-mesh-ui-CsHwj9cJ.umd.cjs} +1 -1
  22. package/dist/{three-mesh-ui-n_qS2BM-.min.js → three-mesh-ui-DhYXcXZe.min.js} +1 -1
  23. package/dist/{three-TNFQHSFa.min.js → three-qw28ZtTy.min.js} +10 -10
  24. package/dist/{vendor-BtJpSuCj.umd.cjs → vendor-D0Yvltn9.umd.cjs} +1 -1
  25. package/dist/{vendor-k9i6CeGi.js → vendor-DU8tJyl_.js} +1 -1
  26. package/dist/{vendor-XJ9xiwrv.min.js → vendor-JyrX4DVM.min.js} +1 -1
  27. package/lib/engine/api.d.ts +1 -0
  28. package/lib/engine/api.js +1 -0
  29. package/lib/engine/api.js.map +1 -1
  30. package/lib/engine/codegen/register_types.js +6 -0
  31. package/lib/engine/codegen/register_types.js.map +1 -1
  32. package/lib/engine/engine_animation.d.ts +21 -1
  33. package/lib/engine/engine_animation.js +32 -1
  34. package/lib/engine/engine_animation.js.map +1 -1
  35. package/lib/engine/engine_camera.d.ts +7 -1
  36. package/lib/engine/engine_camera.fit.d.ts +68 -0
  37. package/lib/engine/engine_camera.fit.js +166 -0
  38. package/lib/engine/engine_camera.fit.js.map +1 -0
  39. package/lib/engine/engine_camera.js +46 -6
  40. package/lib/engine/engine_camera.js.map +1 -1
  41. package/lib/engine/engine_context.d.ts +6 -0
  42. package/lib/engine/engine_context.js +48 -9
  43. package/lib/engine/engine_context.js.map +1 -1
  44. package/lib/engine/engine_gizmos.d.ts +2 -2
  45. package/lib/engine/engine_gizmos.js +2 -2
  46. package/lib/engine/engine_physics.js +6 -3
  47. package/lib/engine/engine_physics.js.map +1 -1
  48. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  49. package/lib/engine/webcomponents/logo-element.js +29 -5
  50. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  51. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  52. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  53. package/lib/engine/webcomponents/needle-engine.d.ts +1 -0
  54. package/lib/engine/webcomponents/needle-engine.js +6 -0
  55. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  56. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  57. package/lib/engine/webcomponents/needle-engine.loading.js +62 -59
  58. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  59. package/lib/engine-components/AnimatorController.js +16 -0
  60. package/lib/engine-components/AnimatorController.js.map +1 -1
  61. package/lib/engine-components/CameraUtils.js +8 -9
  62. package/lib/engine-components/CameraUtils.js.map +1 -1
  63. package/lib/engine-components/OrbitControls.d.ts +7 -47
  64. package/lib/engine-components/OrbitControls.js +25 -149
  65. package/lib/engine-components/OrbitControls.js.map +1 -1
  66. package/lib/engine-components/Renderer.d.ts +2 -2
  67. package/lib/engine-components/Renderer.js +10 -5
  68. package/lib/engine-components/Renderer.js.map +1 -1
  69. package/lib/engine-components/api.d.ts +0 -1
  70. package/lib/engine-components/api.js.map +1 -1
  71. package/lib/engine-components/codegen/components.d.ts +3 -0
  72. package/lib/engine-components/codegen/components.js +3 -0
  73. package/lib/engine-components/codegen/components.js.map +1 -1
  74. package/lib/engine-components/timeline/PlayableDirector.d.ts +35 -6
  75. package/lib/engine-components/timeline/PlayableDirector.js +67 -26
  76. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  77. package/lib/engine-components/timeline/TimelineModels.d.ts +11 -0
  78. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  79. package/lib/engine-components/timeline/TimelineTracks.d.ts +7 -0
  80. package/lib/engine-components/timeline/TimelineTracks.js +23 -2
  81. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  82. package/lib/engine-components/utils/LookAt.js +5 -1
  83. package/lib/engine-components/utils/LookAt.js.map +1 -1
  84. package/lib/engine-components/web/Clickthrough.d.ts +3 -0
  85. package/lib/engine-components/web/Clickthrough.js +13 -2
  86. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  87. package/lib/engine-components/web/CursorFollow.d.ts +3 -0
  88. package/lib/engine-components/web/CursorFollow.js +3 -0
  89. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  90. package/lib/engine-components/web/HoverAnimation.d.ts +44 -0
  91. package/lib/engine-components/web/HoverAnimation.js +105 -0
  92. package/lib/engine-components/web/HoverAnimation.js.map +1 -0
  93. package/lib/engine-components/web/ScrollFollow.d.ts +40 -4
  94. package/lib/engine-components/web/ScrollFollow.js +256 -27
  95. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  96. package/lib/engine-components/web/ViewBox.d.ts +16 -0
  97. package/lib/engine-components/web/ViewBox.js +183 -0
  98. package/lib/engine-components/web/ViewBox.js.map +1 -0
  99. package/lib/engine-components/web/index.d.ts +2 -0
  100. package/lib/engine-components/web/index.js +2 -0
  101. package/lib/engine-components/web/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/plugins/vite/alias.js +5 -3
  104. package/plugins/vite/poster-client.js +22 -21
  105. package/src/engine/api.ts +2 -1
  106. package/src/engine/codegen/register_types.ts +6 -0
  107. package/src/engine/engine_animation.ts +69 -1
  108. package/src/engine/engine_camera.fit.ts +258 -0
  109. package/src/engine/engine_camera.ts +62 -8
  110. package/src/engine/engine_context.ts +50 -10
  111. package/src/engine/engine_gizmos.ts +2 -2
  112. package/src/engine/engine_physics.ts +6 -3
  113. package/src/engine/webcomponents/logo-element.ts +29 -4
  114. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  115. package/src/engine/webcomponents/needle-engine.loading.ts +95 -56
  116. package/src/engine/webcomponents/needle-engine.ts +6 -1
  117. package/src/engine-components/AnimatorController.ts +21 -2
  118. package/src/engine-components/CameraUtils.ts +8 -9
  119. package/src/engine-components/OrbitControls.ts +36 -206
  120. package/src/engine-components/Renderer.ts +10 -5
  121. package/src/engine-components/api.ts +0 -1
  122. package/src/engine-components/codegen/components.ts +3 -0
  123. package/src/engine-components/timeline/PlayableDirector.ts +88 -34
  124. package/src/engine-components/timeline/TimelineModels.ts +11 -0
  125. package/src/engine-components/timeline/TimelineTracks.ts +26 -2
  126. package/src/engine-components/utils/LookAt.ts +5 -1
  127. package/src/engine-components/web/Clickthrough.ts +14 -2
  128. package/src/engine-components/web/CursorFollow.ts +3 -0
  129. package/src/engine-components/web/HoverAnimation.ts +99 -0
  130. package/src/engine-components/web/ScrollFollow.ts +316 -25
  131. package/src/engine-components/web/ViewBox.ts +199 -0
  132. package/src/engine-components/web/index.ts +3 -1
  133. package/dist/needle-engine.bundle-ugr1bBtk.min.js +0 -1616
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "4.9.3",
3
+ "version": "4.10.0-beta.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": {
@@ -113,12 +113,14 @@ export const needleViteAlias = (command, config, userSettings) => {
113
113
 
114
114
  // is needle engine a local package?
115
115
  // This will cause local changes to not be reflected anymore...
116
- const localEnginePath = path.resolve(projectDir, 'node_modules/', '@needle-tools/engine', 'node_modules');
116
+ const localEnginePath = path.resolve(projectDir, 'node_modules/', '@needle-tools/engine', 'node_modules', '.package-lock.json');
117
117
  if (existsSync(localEnginePath)) {
118
118
  config.optimizeDeps ??= {};
119
119
  config.optimizeDeps.exclude ??= [];
120
- config.optimizeDeps.exclude.push('@needle-tools/engine');
121
- log("[needle-alias] Detected local @needle-tools/engine package: will exclude it from optimization");
120
+ if (!config.optimizeDeps.include?.includes('@needle-tools/engine')) {
121
+ config.optimizeDeps.exclude.push('@needle-tools/engine');
122
+ log("[needle-alias] Detected local @needle-tools/engine package → will exclude it from optimization");
123
+ }
122
124
  }
123
125
 
124
126
 
@@ -10,32 +10,33 @@ async function generatePoster() {
10
10
  const height = 1080;
11
11
 
12
12
  return new Promise(res => {
13
- onStart(async context => {
13
+ /** @ts-ignore */
14
+ onStart(async (context) => {
14
15
 
15
16
  if (context.lodsManager.manager) {
16
17
  await context.lodsManager.manager.awaitLoading({ frames: 5, maxPromisesPerObject: 2, waitForFirstCapture: true });
17
18
  }
18
19
 
19
- const mimeType = "image/webp";
20
-
21
- // We're reading back as a blob here because that's async, and doesn't seem
22
- // to stress the GPU so much on memory-constrained devices.
23
- const blob = await screenshot2({ context, width, height, mimeType, type: "blob" });
24
-
25
- // We can only send a DataURL, so we need to convert it back here.
26
- const dataUrl = await new Promise((resolve, reject) => {
27
- const reader = new FileReader();
28
- reader.onload = function () {
29
- resolve(reader.result);
30
- };
31
- reader.onloadend = function () {
32
- resolve(null);
33
- };
34
- reader.readAsDataURL(blob);
35
- });
36
-
37
-
38
- res(dataUrl);
20
+ onStart(() => {
21
+ const mimeType = "image/webp";
22
+ // We're reading back as a blob here because that's async, and doesn't seem
23
+ // to stress the GPU so much on memory-constrained devices.
24
+ screenshot2({ context, width, height, mimeType, type: "blob" })
25
+ // @ts-ignore
26
+ .then(blob => {
27
+ const reader = new FileReader();
28
+ reader.onload = function () {
29
+ res(reader.result);
30
+ };
31
+ reader.onloadend = function () {
32
+ res(null);
33
+ };
34
+ reader.readAsDataURL(blob);
35
+
36
+ })
37
+
38
+
39
+ }, { once: true });
39
40
 
40
41
 
41
42
  }, { once: true });
package/src/engine/api.ts CHANGED
@@ -24,7 +24,8 @@ export * from "./engine_addressables.js";
24
24
  export { AnimationUtils } from "./engine_animation.js";
25
25
  export { Application } from "./engine_application.js";
26
26
  export * from "./engine_assetdatabase.js";
27
- export { getCameraController, setAutoFitEnabled, setCameraController, useForAutoFit } from "./engine_camera.js"
27
+ export * from "./engine_camera.fit.js";
28
+ export { getCameraController, setAutoFitEnabled, setCameraController, useForAutoFit } from "./engine_camera.js";
28
29
  export * from "./engine_components.js";
29
30
  export * from "./engine_components_internal.js";
30
31
  export * from "./engine_components_internal.js";
@@ -108,6 +108,7 @@ import { PlayableDirector } from "../../engine-components/timeline/PlayableDirec
108
108
  import { SignalReceiver } from "../../engine-components/timeline/SignalAsset.js";
109
109
  import { AnimationTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
110
110
  import { AudioTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
111
+ import { MarkerTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
111
112
  import { SignalTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
112
113
  import { ControlTrackHandler } from "../../engine-components/timeline/TimelineTracks.js";
113
114
  import { TransformGizmo } from "../../engine-components/TransformGizmo.js";
@@ -139,7 +140,9 @@ import { VideoPlayer } from "../../engine-components/VideoPlayer.js";
139
140
  import { Voip } from "../../engine-components/Voip.js";
140
141
  import { ClickThrough } from "../../engine-components/web/Clickthrough.js";
141
142
  import { CursorFollow } from "../../engine-components/web/CursorFollow.js";
143
+ import { HoverAnimation } from "../../engine-components/web/HoverAnimation.js";
142
144
  import { ScrollFollow } from "../../engine-components/web/ScrollFollow.js";
145
+ import { ViewBox } from "../../engine-components/web/ViewBox.js";
143
146
  import { Avatar } from "../../engine-components/webxr/Avatar.js";
144
147
  import { XRControllerFollow } from "../../engine-components/webxr/controllers/XRControllerFollow.js";
145
148
  import { XRControllerModel } from "../../engine-components/webxr/controllers/XRControllerModel.js";
@@ -264,6 +267,7 @@ TypeStore.add("PlayableDirector", PlayableDirector);
264
267
  TypeStore.add("SignalReceiver", SignalReceiver);
265
268
  TypeStore.add("AnimationTrackHandler", AnimationTrackHandler);
266
269
  TypeStore.add("AudioTrackHandler", AudioTrackHandler);
270
+ TypeStore.add("MarkerTrackHandler", MarkerTrackHandler);
267
271
  TypeStore.add("SignalTrackHandler", SignalTrackHandler);
268
272
  TypeStore.add("ControlTrackHandler", ControlTrackHandler);
269
273
  TypeStore.add("TransformGizmo", TransformGizmo);
@@ -295,7 +299,9 @@ TypeStore.add("VideoPlayer", VideoPlayer);
295
299
  TypeStore.add("Voip", Voip);
296
300
  TypeStore.add("ClickThrough", ClickThrough);
297
301
  TypeStore.add("CursorFollow", CursorFollow);
302
+ TypeStore.add("HoverAnimation", HoverAnimation);
298
303
  TypeStore.add("ScrollFollow", ScrollFollow);
304
+ TypeStore.add("ViewBox", ViewBox);
299
305
  TypeStore.add("Avatar", Avatar);
300
306
  TypeStore.add("XRControllerFollow", XRControllerFollow);
301
307
  TypeStore.add("XRControllerModel", XRControllerModel);
@@ -1,4 +1,4 @@
1
- import { AnimationAction, AnimationClip, AnimationMixer, Object3D, PropertyBinding } from "three";
1
+ import { AnimationAction, AnimationClip, AnimationMixer, KeyframeTrack, Object3D, PropertyBinding, Vector3Like } from "three";
2
2
 
3
3
  import type { Context } from "./engine_context.js";
4
4
  import { GLTF, IAnimationComponent, Model } from "./engine_types.js";
@@ -151,4 +151,72 @@ export class AnimationUtils {
151
151
  return findAnimationComponentInParent(obj.parent);
152
152
  }
153
153
  }
154
+
155
+
156
+ static emptyClip(): AnimationClip {
157
+ return new AnimationClip("empty", 0, []);
158
+ }
159
+
160
+ static createScaleClip(options?: ScaleClipOptions): AnimationClip {
161
+ const duration = options?.duration ?? 0.3;
162
+
163
+ let baseScale: Vector3Like = { x: 1, y: 1, z: 1 };
164
+ if (options?.scale !== undefined) {
165
+ if (typeof options.scale === "number") {
166
+ baseScale = { x: options.scale, y: options.scale, z: options.scale };
167
+ }
168
+ else {
169
+ baseScale = options.scale;
170
+ }
171
+ }
172
+ const type = options?.type ?? "linear";
173
+ const scale = options?.scaleFactor ?? 1.2;
174
+
175
+ const times = new Array<number>();
176
+ const values = new Array<number>();
177
+ switch (type) {
178
+ case "linear":
179
+ times.push(0, duration);
180
+ values.push(
181
+ baseScale.x, baseScale.y, baseScale.z,
182
+ baseScale.x * scale, baseScale.y * scale, baseScale.z * scale
183
+ );
184
+ break;
185
+
186
+ case "spring":
187
+ times.push(0, duration * 0.3, duration * 0.5, duration * 0.7, duration * 0.9, duration);
188
+ values.push(
189
+ baseScale.x, baseScale.y, baseScale.z,
190
+ baseScale.x * scale, baseScale.y * scale, baseScale.z * scale,
191
+ baseScale.x * 0.9, baseScale.y * 0.9, baseScale.z * 0.9,
192
+ baseScale.x * 1.05, baseScale.y * 1.05, baseScale.z * 1.05,
193
+ baseScale.x * 0.98, baseScale.y * 0.98, baseScale.z * 0.98,
194
+ baseScale.x, baseScale.y, baseScale.z
195
+ );
196
+ break;
197
+
198
+ }
199
+
200
+ const track = new KeyframeTrack(".scale", times, values);
201
+ return new AnimationClip("scale", times[times.length - 1], [track]);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Type of scale animation to create.
207
+ * - "linear": Simple linear scale up animation.
208
+ * - "spring": Spring-like scale animation with overshoot and settling.
209
+ */
210
+ export type ScaleClipType = "linear" | "spring";
211
+
212
+ type ScaleClipOptions = {
213
+ /**
214
+ * Type of scale animation to create.
215
+ * - "linear": Simple linear scale up animation.
216
+ * - "spring": Spring-like scale animation with overshoot and settling.
217
+ */
218
+ type?: ScaleClipType,
219
+ duration?: number,
220
+ scale?: number | Vector3Like,
221
+ scaleFactor?: number
154
222
  }
@@ -0,0 +1,258 @@
1
+ import { Camera, Object3D, PerspectiveCamera, Vector3, Vector3Like } from "three";
2
+
3
+ import { GroundProjectedEnv } from "../engine-components/GroundProjection.js";
4
+ import { findObjectOfType } from "./engine_components.js";
5
+ import { Context } from "./engine_context.js";
6
+ import { Gizmos } from "./engine_gizmos.js";
7
+ import { getBoundingBox } from "./engine_three_utils.js";
8
+ import { NeedleXRSession } from "./xr/NeedleXRSession.js";
9
+
10
+
11
+ /**
12
+ * Options for fitting the camera to the scene. Used in {@link OrbitControls.fitCamera}
13
+ */
14
+ export type FitCameraOptions = {
15
+ /** When enabled debug rendering will be shown */
16
+ debug?: boolean,
17
+
18
+ /**
19
+ * If true the camera position and target will be applied immediately
20
+ * @default true
21
+ */
22
+ autoApply?: boolean,
23
+
24
+ /**
25
+ * The context to use. If not provided the current context will be used
26
+ */
27
+ context?: Context,
28
+
29
+ /**
30
+ * The camera to fit. If not provided the current camera will be used
31
+ */
32
+ camera?: Camera,
33
+
34
+ currentZoom?: number,
35
+ minZoom?: number,
36
+ maxZoom?: number,
37
+
38
+ /**
39
+ * The objects to fit the camera to. If not provided the scene children will be used
40
+ */
41
+ objects?: Object3D[] | Object3D;
42
+
43
+ /** Fit offset: A factor to multiply the distance to the objects by
44
+ * @default 1.1
45
+ */
46
+ fitOffset?: number,
47
+
48
+ /** The direction from which the camera should be fitted in worldspace. If not defined the current camera's position will be used */
49
+ fitDirection?: Vector3Like,
50
+
51
+ /** If set to "y" the camera will be centered in the y axis */
52
+ centerCamera?: "none" | "y",
53
+ /** Set to 'auto' to update the camera near or far plane based on the fitted-objects bounds */
54
+ cameraNearFar?: "keep" | "auto",
55
+
56
+ /**
57
+ * Offset the camera position in world space
58
+ */
59
+ cameraOffset?: Partial<Vector3Like>,
60
+ /**
61
+ * Offset the camera position relative to the size of the objects being focused on (e.g. x: 0.5).
62
+ * Value range: -1 to 1
63
+ */
64
+ relativeCameraOffset?: Partial<Vector3Like>,
65
+
66
+ /**
67
+ * Offset the camera target position in world space
68
+ */
69
+ targetOffset?: Partial<Vector3Like>,
70
+ /**
71
+ * Offset the camera target position relative to the size of the objects being focused on.
72
+ * Value range: -1 to 1
73
+ */
74
+ relativeTargetOffset?: Partial<Vector3Like>,
75
+
76
+ /**
77
+ * Field of view (FOV) for the camera
78
+ */
79
+ fov?: number,
80
+ }
81
+
82
+ export type FitCameraReturnType = {
83
+ camera: Camera,
84
+ position: Vector3,
85
+ lookAt: Vector3,
86
+ fov: number | undefined
87
+ }
88
+
89
+
90
+ export function fitCamera(options?: FitCameraOptions): null | FitCameraReturnType {
91
+
92
+ if (NeedleXRSession.active) {
93
+ // camera fitting in XR is not supported
94
+ console.warn('[OrbitControls] Can not fit camera while XR session is active');
95
+ return null;
96
+ }
97
+
98
+ const context = Context.Current;
99
+ if (!context) {
100
+ console.warn('[OrbitControls] No context found');
101
+ return null;
102
+ }
103
+ const camera = options?.camera || context.mainCamera;
104
+
105
+ // const controls = this._controls as ThreeOrbitControls | null;
106
+
107
+ if (!camera) {
108
+ console.warn("No camera or controls found to fit camera to objects...");
109
+ return null;
110
+ }
111
+
112
+ if (!options) options = {}
113
+ options.autoApply = options.autoApply !== false; // default to true
114
+ options.minZoom ||= 0;
115
+ options.maxZoom ||= Infinity;
116
+
117
+ const {
118
+ centerCamera,
119
+ cameraNearFar = "auto",
120
+ fitOffset = 1.1,
121
+ fov = camera instanceof PerspectiveCamera ? camera?.fov : -1
122
+ } = options;
123
+
124
+ const size = new Vector3();
125
+ const center = new Vector3();
126
+ const aspect = camera instanceof PerspectiveCamera ? camera.aspect : 1;
127
+ const objects = options.objects || context.scene;
128
+ // TODO would be much better to calculate the bounds in camera space instead of world space -
129
+ // we would get proper view-dependant fit.
130
+ // Right now it's independent from where the camera is actually looking from,
131
+ // and thus we're just getting some maximum that will work for sure.
132
+ const box = getBoundingBox(objects, undefined, camera?.layers);
133
+ const boxCopy = box.clone();
134
+ box.getCenter(center);
135
+
136
+ const box_size = new Vector3();
137
+ box.getSize(box_size);
138
+
139
+ // project this box into camera space
140
+ if (camera instanceof PerspectiveCamera) camera.updateProjectionMatrix();
141
+ camera.updateMatrixWorld();
142
+ box.applyMatrix4(camera.matrixWorldInverse);
143
+
144
+ box.getSize(size);
145
+ box.setFromCenterAndSize(center, size);
146
+ if (Number.isNaN(size.x) || Number.isNaN(size.y) || Number.isNaN(size.z)) {
147
+ console.warn("Camera fit size resultet in NaN", camera, box);
148
+ return null;
149
+ }
150
+ if (size.length() <= 0.0000000001) {
151
+ console.warn("Camera fit size is zero", box);
152
+ return null;
153
+ }
154
+
155
+ const verticalFov = fov;
156
+ const horizontalFov = 2 * Math.atan(Math.tan(verticalFov * Math.PI / 360 / 2) * aspect) / Math.PI * 360;
157
+ const fitHeightDistance = size.y / (2 * Math.atan(Math.PI * verticalFov / 360));
158
+ const fitWidthDistance = size.x / (2 * Math.atan(Math.PI * horizontalFov / 360));
159
+
160
+ const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + size.z / 2;
161
+ options.maxZoom = distance * 10;
162
+ options.minZoom = distance * 0.01;
163
+
164
+ if (options.debug === true) {
165
+ console.log("Fit camera to objects", { fitHeightDistance, fitWidthDistance, distance, verticalFov, horizontalFov });
166
+ }
167
+
168
+ const verticalOffset = 0.05;
169
+ const lookAt = center.clone();
170
+ lookAt.y -= size.y * verticalOffset;
171
+ if (options.targetOffset) {
172
+ if (options.targetOffset.x !== undefined) lookAt.x += options.targetOffset.x;
173
+ if (options.targetOffset.y !== undefined) lookAt.y += options.targetOffset.y;
174
+ if (options.targetOffset.z !== undefined) lookAt.z += options.targetOffset.z;
175
+ }
176
+ if (options.relativeTargetOffset) {
177
+ if (options.relativeTargetOffset.x !== undefined) lookAt.x += options.relativeTargetOffset.x * size.x;
178
+ if (options.relativeTargetOffset.y !== undefined) lookAt.y += options.relativeTargetOffset.y * size.y;
179
+ if (options.relativeTargetOffset.z !== undefined) lookAt.z += options.relativeTargetOffset.z * size.z;
180
+ }
181
+ // this.setLookTargetPosition(lookAt, immediate);
182
+ // this.setFieldOfView(options.fov, immediate);
183
+
184
+ if (cameraNearFar == undefined || cameraNearFar == "auto") {
185
+ // Check if the scene has a GroundProjectedEnv and include the scale to the far plane so that it doesnt cut off
186
+ const groundprojection = findObjectOfType(GroundProjectedEnv);
187
+ const groundProjectionRadius = groundprojection ? groundprojection.radius : 0;
188
+ const boundsMax = Math.max(box_size.x, box_size.y, box_size.z, groundProjectionRadius);
189
+ // TODO: this doesnt take the Camera component nearClipPlane into account
190
+ if (camera instanceof PerspectiveCamera) {
191
+ camera.near = (distance / 100);
192
+ camera.far = boundsMax + distance * 10;
193
+ camera.updateProjectionMatrix();
194
+ }
195
+
196
+ // adjust maxZoom so that the ground projection radius is always inside
197
+ if (groundprojection) {
198
+ options.maxZoom = Math.max(Math.min(options.maxZoom, groundProjectionRadius * 0.5), distance);
199
+ }
200
+ }
201
+
202
+ // ensure we're not clipping out of the current zoom level just because we're fitting
203
+ if (options.currentZoom !== undefined) {
204
+ if (options.currentZoom < options.minZoom) options.minZoom = options.currentZoom * 0.9;
205
+ if (options.currentZoom > options.maxZoom) options.maxZoom = options.currentZoom * 1.1;
206
+ }
207
+
208
+ const direction = center.clone();
209
+ if (options.fitDirection) {
210
+ direction.sub(new Vector3().copy(options.fitDirection).multiplyScalar(1_000_000));
211
+ }
212
+ else {
213
+ direction.sub(camera.worldPosition);
214
+ }
215
+ if (centerCamera === "y")
216
+ direction.y = 0;
217
+ direction.normalize();
218
+ direction.multiplyScalar(distance);
219
+ if (centerCamera === "y")
220
+ direction.y += -verticalOffset * 4 * distance;
221
+
222
+ let cameraLocalPosition = center.clone().sub(direction);
223
+ if (options.cameraOffset) {
224
+ if (options.cameraOffset.x !== undefined) cameraLocalPosition.x += options.cameraOffset.x;
225
+ if (options.cameraOffset.y !== undefined) cameraLocalPosition.y += options.cameraOffset.y;
226
+ if (options.cameraOffset.z !== undefined) cameraLocalPosition.z += options.cameraOffset.z;
227
+ }
228
+ if (options.relativeCameraOffset) {
229
+ if (options.relativeCameraOffset.x !== undefined) cameraLocalPosition.x += options.relativeCameraOffset.x * size.x;
230
+ if (options.relativeCameraOffset.y !== undefined) cameraLocalPosition.y += options.relativeCameraOffset.y * size.y;
231
+ if (options.relativeCameraOffset.z !== undefined) cameraLocalPosition.z += options.relativeCameraOffset.z * size.z;
232
+ }
233
+ if (camera.parent) {
234
+ cameraLocalPosition = camera.parent.worldToLocal(cameraLocalPosition);
235
+ }
236
+ // this.setCameraTargetPosition(cameraLocalPosition, immediate);
237
+
238
+ if (options.debug) {
239
+ Gizmos.DrawWireBox3(box, 0xffff33, 10);
240
+ Gizmos.DrawWireBox3(boxCopy, 0x00ff00, 10);
241
+ }
242
+
243
+ if (options.autoApply) {
244
+ camera.position.copy(cameraLocalPosition);
245
+ camera.lookAt(lookAt);
246
+ if (fov > 0 && camera instanceof PerspectiveCamera) {
247
+ camera.fov = fov;
248
+ camera.updateProjectionMatrix();
249
+ }
250
+ }
251
+
252
+ return {
253
+ camera: camera,
254
+ position: cameraLocalPosition,
255
+ lookAt: lookAt,
256
+ fov: options.fov,
257
+ }
258
+ }
@@ -52,15 +52,23 @@ export type FocusRectSettings = {
52
52
  /** Lower values will result in faster alignment with the rect (value ~= seconds to reach target)
53
53
  * Minimum value is 0.
54
54
  */
55
- damping: number
55
+ damping: number,
56
+
57
+ /** X offset in camera coordinates. Used by ViewBox component */
58
+ offsetX: number,
59
+ /** Y offset in camera coordinates. Used by ViewBox component */
60
+ offsetY: number,
61
+ /** Zoom factor. Used by ViewBox component */
62
+ zoom: number,
56
63
  }
57
64
  export type FocusRect = DOMRect | Element | { x: number, y: number, width: number, height: number };
58
65
 
59
66
  let rendererRect: DOMRect | undefined = undefined;
60
67
  const overlapRect = { x: 0, y: 0, width: 0, height: 0 };
68
+ let _testTime = 1;
61
69
 
62
70
  /** Used internally by the Needle Engine context via 'setFocusRect(<rect>)' */
63
- export function updateCameraFocusRect(focusRect: FocusRect, dt: number, camera: PerspectiveCamera, renderer: WebGLRenderer) {
71
+ export function updateCameraFocusRect(focusRect: FocusRect, settings: FocusRectSettings, dt: number, camera: PerspectiveCamera, renderer: WebGLRenderer) {
64
72
 
65
73
  if (focusRect instanceof Element) {
66
74
  focusRect = focusRect.getBoundingClientRect();
@@ -76,16 +84,62 @@ export function updateCameraFocusRect(focusRect: FocusRect, dt: number, camera:
76
84
  rect.x -= rendererRect.x;
77
85
  rect.y -= rendererRect.y;
78
86
 
79
- const targetX = rect.width / -2 - (rect.x - (rendererRect.width / 2));
80
- const targetY = rect.height / -2 - (rect.y - (rendererRect.height / 2));
87
+ const sourceWidth = rendererRect.width;
88
+ const sourceHeight = rendererRect.height;
81
89
 
82
- const view = camera.view;
90
+ const view = camera.view as PerspectiveCamera["view"];
83
91
 
92
+ // Apply zoom
93
+ let zoom = settings.zoom;
84
94
  let offsetX = view?.offsetX || 0;
85
95
  let offsetY = view?.offsetY || 0;
86
- offsetX = Mathf.lerp(offsetX, targetX, dt);
87
- offsetY = Mathf.lerp(offsetY, targetY, dt);
88
96
 
89
- camera.setViewOffset(rendererRect.width, rendererRect.height, offsetX, offsetY, rendererRect.width, rendererRect.height);
97
+ let width = rendererRect.width;
98
+ let height = rendererRect.height;
99
+ width /= zoom;
100
+ height /= zoom;
101
+ offsetX = width * (zoom - 1) * .5;
102
+ offsetY = height * (zoom - 1) * .5;
103
+
104
+ const focusRectCenterX = rect.x + rect.width * .5;
105
+ const focusRectCenterY = rect.y + rect.height * .5;
106
+ const rendererCenterX = rendererRect.width * .5;
107
+ const rendererCenterY = rendererRect.height * .5;
108
+
109
+ const diffx = focusRectCenterX - rendererCenterX;
110
+ const diffy = focusRectCenterY - rendererCenterY;
111
+ offsetX -= diffx / zoom;
112
+ offsetY -= diffy / zoom;
113
+ if (settings.offsetX !== undefined) {
114
+ offsetX += settings.offsetX * (rendererRect.width * .5);
115
+ }
116
+ if (settings.offsetY !== undefined) {
117
+ offsetY -= settings.offsetY * (rendererRect.height * .5);
118
+ }
119
+
120
+
121
+ const currentOffsetX = view?.offsetX || offsetX;
122
+ const currentOffsetY = view?.offsetY || offsetY;
123
+ offsetX = Mathf.lerp(currentOffsetX, offsetX, dt);
124
+ offsetY = Mathf.lerp(currentOffsetY, offsetY, dt);
125
+ const currentWidth = view?.width || sourceWidth;
126
+ const currentHeight = view?.height || sourceHeight;
127
+ width = Mathf.lerp(currentWidth, width, dt);
128
+ height = Mathf.lerp(currentHeight, height, dt);
129
+
130
+ camera.setViewOffset(sourceWidth, sourceHeight, offsetX, offsetY, width, height);
90
131
  camera.updateProjectionMatrix();
132
+
133
+ if (settings.damping > 0) {
134
+ settings.damping *= (1.0 - dt);
135
+ if (settings.damping < 0.01) settings.damping = 0;
136
+ settings.damping = Math.max(0, settings.damping);
137
+ }
138
+ }
139
+
140
+
141
+ function fit(width1: number, height1: number, width2: number, height2: number) {
142
+ const scaleX = width2 / width1;
143
+ const scaleY = height2 / height1;
144
+ return Math.max(scaleX, scaleY);
91
145
  }
@@ -1382,21 +1382,61 @@ export class Context implements IContext {
1382
1382
  * @param settings Optional settings for the focus rect. These will override the `focusRectSettings` property
1383
1383
  */
1384
1384
  public setCameraFocusRect(rect: FocusRect | null, settings?: Partial<FocusRectSettings>) {
1385
+ const oldRect = this._focusRect;
1385
1386
  this._focusRect = rect;
1386
1387
  if (settings) {
1387
1388
  Object.assign(this.focusRectSettings, settings);
1388
1389
  }
1390
+ if (settings?.damping === undefined) {
1391
+ // if the new rect is on screen then set damping
1392
+ if (oldRect) {
1393
+ let domRect = oldRect as DOMRect;
1394
+ if (oldRect instanceof HTMLElement) {
1395
+ domRect = oldRect.getBoundingClientRect();
1396
+ }
1397
+ if (domRect && "top" in domRect) {
1398
+ const allowedDistance = 100;
1399
+ const isVisible = domRect.bottom >= -allowedDistance && domRect.right >= -allowedDistance && domRect.top <= window.innerHeight + allowedDistance && domRect.left <= window.innerWidth + allowedDistance;
1400
+ if (isVisible) this.focusRectSettings.damping = .2;
1401
+ }
1402
+ }
1403
+ }
1404
+
1389
1405
  }
1390
1406
  get focusRect() { return this._focusRect; }
1407
+ get focusRectSize(): null | { x: number, y: number, width: number, height: number } {
1408
+ const rect = this._focusRect;
1409
+ if (rect && (rect instanceof DOMRect || ("width" in rect && "height" in rect && "x" in rect && "y" in rect))) {
1410
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
1411
+ }
1412
+ else if (rect instanceof HTMLElement) {
1413
+ const r = rect.getBoundingClientRect();
1414
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
1415
+ }
1416
+ return null;
1417
+ }
1391
1418
  /** Settings when a focus rect is set. Use `setCameraFocusRect(...)` to do so.
1392
1419
  * This can be used to offset the renderer center e.g. to a specific DOM element.
1393
1420
  */
1394
1421
  readonly focusRectSettings: FocusRectSettings = {
1395
1422
  /** Controls how fast the rect is centered. Smaller values mean the rect is centered faster.
1396
1423
  * A minimum value of 0 means the rect is centered instantly.
1397
- * @default .05
1424
+ * @default 0
1398
1425
  */
1399
- damping: .05
1426
+ damping: 0,
1427
+
1428
+ /**
1429
+ * Zoom factor when a focus rect is set.
1430
+ */
1431
+ zoom: 1,
1432
+ /**
1433
+ * Additional offset in pixels from the center of the rect
1434
+ */
1435
+ offsetX: 0,
1436
+ /**
1437
+ * Additional offset in pixels from the center of the rect
1438
+ */
1439
+ offsetY: 0,
1400
1440
  };
1401
1441
  private _focusRect: FocusRect | null = null;
1402
1442
 
@@ -1528,14 +1568,6 @@ export class Context implements IContext {
1528
1568
 
1529
1569
  if (this.isVisibleToUser || this.runInBackground) {
1530
1570
 
1531
- if (this._focusRect) {
1532
- if (this.mainCamera instanceof PerspectiveCamera) {
1533
- const settings = this.focusRectSettings;
1534
- const dt = settings.damping > 0 ? this.time.deltaTime / settings.damping : 1;
1535
- updateCameraFocusRect(this._focusRect, dt, this.mainCamera, this.renderer);
1536
- }
1537
- }
1538
-
1539
1571
  this._currentFrameEvent = FrameEvent.OnBeforeRender;
1540
1572
 
1541
1573
  // should we move these callbacks in the regular three onBeforeRender events?
@@ -1552,6 +1584,14 @@ export class Context implements IContext {
1552
1584
  this.executeCoroutines(FrameEvent.OnBeforeRender);
1553
1585
  invokeLifecycleFunctions(this, FrameEvent.OnBeforeRender);
1554
1586
 
1587
+ if (this._focusRect) {
1588
+ if (this.mainCamera instanceof PerspectiveCamera) {
1589
+ const settings = this.focusRectSettings;
1590
+ const dt = settings.damping > 0 ? this.time.deltaTime / settings.damping : 1;
1591
+ updateCameraFocusRect(this._focusRect, this.focusRectSettings, dt, this.mainCamera, this.renderer);
1592
+ }
1593
+ }
1594
+
1555
1595
  if (this._needsUpdateSize)
1556
1596
  this.updateSize();
1557
1597
 
@@ -230,8 +230,8 @@ export class Gizmos {
230
230
  * Draw a 3D wiremesh box gizmo in the scene
231
231
  * @param box the box in world space
232
232
  * @param color the color of the box
233
- * @param duration the duration in seconds the box will be rendered. If 0 it will be rendered for one frame
234
- * @param depthTest if true the box will be rendered with depth test
233
+ * @param duration the duration in seconds the box will be rendered. If 0 it will be rendered for one frame. Default: 0
234
+ * @param depthTest if true the box will be rendered with depth test. Default: true
235
235
  */
236
236
  static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
237
237
  if (!Gizmos.enabled) return;