@needle-tools/engine 4.9.3 → 4.10.0-beta

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 (110) 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-42AmEGfk.umd.cjs} +160 -137
  7. package/dist/needle-engine.bundle-C6zhyLF5.min.js +1639 -0
  8. package/dist/{needle-engine.bundle-BAsxNKpA.js → needle-engine.bundle-Dj6faVbC.js} +7665 -7380
  9. package/dist/needle-engine.js +447 -444
  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 +4 -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.fit.d.ts +68 -0
  36. package/lib/engine/engine_camera.fit.js +193 -0
  37. package/lib/engine/engine_camera.fit.js.map +1 -0
  38. package/lib/engine/engine_gizmos.d.ts +2 -2
  39. package/lib/engine/engine_gizmos.js +2 -2
  40. package/lib/engine/engine_physics.js +6 -3
  41. package/lib/engine/engine_physics.js.map +1 -1
  42. package/lib/engine/webcomponents/needle-engine.d.ts +1 -0
  43. package/lib/engine/webcomponents/needle-engine.js +6 -0
  44. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  45. package/lib/engine/webcomponents/needle-engine.loading.js +59 -23
  46. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  47. package/lib/engine-components/AnimatorController.js +16 -0
  48. package/lib/engine-components/AnimatorController.js.map +1 -1
  49. package/lib/engine-components/CameraUtils.js +8 -9
  50. package/lib/engine-components/CameraUtils.js.map +1 -1
  51. package/lib/engine-components/OrbitControls.d.ts +4 -47
  52. package/lib/engine-components/OrbitControls.js +30 -178
  53. package/lib/engine-components/OrbitControls.js.map +1 -1
  54. package/lib/engine-components/Renderer.d.ts +2 -2
  55. package/lib/engine-components/Renderer.js +6 -4
  56. package/lib/engine-components/Renderer.js.map +1 -1
  57. package/lib/engine-components/api.d.ts +0 -1
  58. package/lib/engine-components/api.js.map +1 -1
  59. package/lib/engine-components/codegen/components.d.ts +2 -0
  60. package/lib/engine-components/codegen/components.js +2 -0
  61. package/lib/engine-components/codegen/components.js.map +1 -1
  62. package/lib/engine-components/timeline/PlayableDirector.d.ts +28 -6
  63. package/lib/engine-components/timeline/PlayableDirector.js +60 -26
  64. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  65. package/lib/engine-components/timeline/TimelineModels.d.ts +3 -0
  66. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  67. package/lib/engine-components/timeline/TimelineTracks.d.ts +7 -0
  68. package/lib/engine-components/timeline/TimelineTracks.js +19 -0
  69. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  70. package/lib/engine-components/web/Clickthrough.d.ts +3 -0
  71. package/lib/engine-components/web/Clickthrough.js +3 -0
  72. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  73. package/lib/engine-components/web/CursorFollow.d.ts +3 -0
  74. package/lib/engine-components/web/CursorFollow.js +3 -0
  75. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  76. package/lib/engine-components/web/HoverAnimation.d.ts +44 -0
  77. package/lib/engine-components/web/HoverAnimation.js +105 -0
  78. package/lib/engine-components/web/HoverAnimation.js.map +1 -0
  79. package/lib/engine-components/web/ScrollFollow.d.ts +18 -4
  80. package/lib/engine-components/web/ScrollFollow.js +143 -25
  81. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  82. package/lib/engine-components/web/index.d.ts +1 -0
  83. package/lib/engine-components/web/index.js +1 -0
  84. package/lib/engine-components/web/index.js.map +1 -1
  85. package/package.json +1 -1
  86. package/plugins/vite/alias.js +5 -3
  87. package/plugins/vite/poster-client.js +22 -21
  88. package/src/engine/api.ts +2 -1
  89. package/src/engine/codegen/register_types.ts +4 -0
  90. package/src/engine/engine_animation.ts +69 -1
  91. package/src/engine/engine_camera.fit.ts +288 -0
  92. package/src/engine/engine_gizmos.ts +2 -2
  93. package/src/engine/engine_physics.ts +6 -3
  94. package/src/engine/webcomponents/needle-engine.loading.ts +63 -24
  95. package/src/engine/webcomponents/needle-engine.ts +6 -1
  96. package/src/engine-components/AnimatorController.ts +21 -2
  97. package/src/engine-components/CameraUtils.ts +8 -9
  98. package/src/engine-components/OrbitControls.ts +30 -239
  99. package/src/engine-components/Renderer.ts +6 -4
  100. package/src/engine-components/api.ts +0 -1
  101. package/src/engine-components/codegen/components.ts +2 -0
  102. package/src/engine-components/timeline/PlayableDirector.ts +79 -34
  103. package/src/engine-components/timeline/TimelineModels.ts +3 -0
  104. package/src/engine-components/timeline/TimelineTracks.ts +22 -0
  105. package/src/engine-components/web/Clickthrough.ts +3 -0
  106. package/src/engine-components/web/CursorFollow.ts +3 -0
  107. package/src/engine-components/web/HoverAnimation.ts +99 -0
  108. package/src/engine-components/web/ScrollFollow.ts +181 -24
  109. package/src/engine-components/web/index.ts +1 -0
  110. package/dist/needle-engine.bundle-ugr1bBtk.min.js +0 -1616
@@ -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,6 +140,7 @@ 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";
143
145
  import { Avatar } from "../../engine-components/webxr/Avatar.js";
144
146
  import { XRControllerFollow } from "../../engine-components/webxr/controllers/XRControllerFollow.js";
@@ -264,6 +266,7 @@ TypeStore.add("PlayableDirector", PlayableDirector);
264
266
  TypeStore.add("SignalReceiver", SignalReceiver);
265
267
  TypeStore.add("AnimationTrackHandler", AnimationTrackHandler);
266
268
  TypeStore.add("AudioTrackHandler", AudioTrackHandler);
269
+ TypeStore.add("MarkerTrackHandler", MarkerTrackHandler);
267
270
  TypeStore.add("SignalTrackHandler", SignalTrackHandler);
268
271
  TypeStore.add("ControlTrackHandler", ControlTrackHandler);
269
272
  TypeStore.add("TransformGizmo", TransformGizmo);
@@ -295,6 +298,7 @@ TypeStore.add("VideoPlayer", VideoPlayer);
295
298
  TypeStore.add("Voip", Voip);
296
299
  TypeStore.add("ClickThrough", ClickThrough);
297
300
  TypeStore.add("CursorFollow", CursorFollow);
301
+ TypeStore.add("HoverAnimation", HoverAnimation);
298
302
  TypeStore.add("ScrollFollow", ScrollFollow);
299
303
  TypeStore.add("Avatar", Avatar);
300
304
  TypeStore.add("XRControllerFollow", XRControllerFollow);
@@ -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,288 @@
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(objectsOrOptions?: Object3D | Array<Object3D> | FitCameraOptions, 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
+ let objects: Object3D | Array<Object3D> | undefined = undefined;
106
+ // If the user passed in an array as first argument
107
+ if (Array.isArray(objectsOrOptions)) {
108
+ objects = objectsOrOptions;
109
+ }
110
+ // If the user passed in an object as first argument
111
+ else if (objectsOrOptions && "type" in objectsOrOptions) {
112
+ objects = objectsOrOptions;
113
+ }
114
+ // If the user passed in an object as first argument and options as second argument
115
+ else if (objectsOrOptions && typeof objectsOrOptions === "object") {
116
+ if (!(objectsOrOptions instanceof Object3D) && !Array.isArray(objectsOrOptions)) {
117
+ options = objectsOrOptions;
118
+ objects = options.objects;
119
+ }
120
+ }
121
+ // Ensure objects are setup correctly
122
+ if (objects && !Array.isArray(objects)) {
123
+ objects = [objects];
124
+ }
125
+ if (!Array.isArray(objects) || objects && objects.length <= 0) {
126
+ objects = context.scene.children;
127
+ }
128
+
129
+ // Make sure there's anything to fit to
130
+ if (!Array.isArray(objects) || objects.length <= 0) {
131
+ console.warn("No objects to fit camera to...");
132
+ return null;
133
+ }
134
+
135
+
136
+ // const controls = this._controls as ThreeOrbitControls | null;
137
+
138
+ if (!camera) {
139
+ console.warn("No camera or controls found to fit camera to objects...");
140
+ return null;
141
+ }
142
+
143
+ if (!options) options = {}
144
+ options.autoApply = options.autoApply !== false; // default to true
145
+ options.minZoom ||= 0;
146
+ options.maxZoom ||= Infinity;
147
+
148
+ const {
149
+ centerCamera,
150
+ cameraNearFar = "auto",
151
+ fitOffset = 1.1,
152
+ fov = camera instanceof PerspectiveCamera ? camera?.fov : -1
153
+ } = options;
154
+
155
+ const size = new Vector3();
156
+ const center = new Vector3();
157
+ const aspect = camera instanceof PerspectiveCamera ? camera.aspect : 1;
158
+ // TODO would be much better to calculate the bounds in camera space instead of world space -
159
+ // we would get proper view-dependant fit.
160
+ // Right now it's independent from where the camera is actually looking from,
161
+ // and thus we're just getting some maximum that will work for sure.
162
+ const box = getBoundingBox(objects, undefined, camera?.layers);
163
+ const boxCopy = box.clone();
164
+ box.getCenter(center);
165
+
166
+ const box_size = new Vector3();
167
+ box.getSize(box_size);
168
+
169
+ // project this box into camera space
170
+ if (camera instanceof PerspectiveCamera) camera.updateProjectionMatrix();
171
+ camera.updateMatrixWorld();
172
+ box.applyMatrix4(camera.matrixWorldInverse);
173
+
174
+ box.getSize(size);
175
+ box.setFromCenterAndSize(center, size);
176
+ if (Number.isNaN(size.x) || Number.isNaN(size.y) || Number.isNaN(size.z)) {
177
+ console.warn("Camera fit size resultet in NaN", camera, box);
178
+ return null;
179
+ }
180
+ if (size.length() <= 0.0000000001) {
181
+ console.warn("Camera fit size is zero", box);
182
+ return null;
183
+ }
184
+
185
+ const verticalFov = fov;
186
+ const horizontalFov = 2 * Math.atan(Math.tan(verticalFov * Math.PI / 360 / 2) * aspect) / Math.PI * 360;
187
+ const fitHeightDistance = size.y / (2 * Math.atan(Math.PI * verticalFov / 360));
188
+ const fitWidthDistance = size.x / (2 * Math.atan(Math.PI * horizontalFov / 360));
189
+
190
+ const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + size.z / 2;
191
+ options.maxZoom = distance * 10;
192
+ options.minZoom = distance * 0.01;
193
+
194
+ if (options.debug === true) {
195
+ console.log("Fit camera to objects", { fitHeightDistance, fitWidthDistance, distance, verticalFov, horizontalFov });
196
+ }
197
+
198
+ const verticalOffset = 0.05;
199
+ const lookAt = center.clone();
200
+ lookAt.y -= size.y * verticalOffset;
201
+ if (options.targetOffset) {
202
+ if (options.targetOffset.x !== undefined) lookAt.x += options.targetOffset.x;
203
+ if (options.targetOffset.y !== undefined) lookAt.y += options.targetOffset.y;
204
+ if (options.targetOffset.z !== undefined) lookAt.z += options.targetOffset.z;
205
+ }
206
+ if (options.relativeTargetOffset) {
207
+ if (options.relativeTargetOffset.x !== undefined) lookAt.x += options.relativeTargetOffset.x * size.x;
208
+ if (options.relativeTargetOffset.y !== undefined) lookAt.y += options.relativeTargetOffset.y * size.y;
209
+ if (options.relativeTargetOffset.z !== undefined) lookAt.z += options.relativeTargetOffset.z * size.z;
210
+ }
211
+ // this.setLookTargetPosition(lookAt, immediate);
212
+ // this.setFieldOfView(options.fov, immediate);
213
+
214
+ if (cameraNearFar == undefined || cameraNearFar == "auto") {
215
+ // Check if the scene has a GroundProjectedEnv and include the scale to the far plane so that it doesnt cut off
216
+ const groundprojection = findObjectOfType(GroundProjectedEnv);
217
+ const groundProjectionRadius = groundprojection ? groundprojection.radius : 0;
218
+ const boundsMax = Math.max(box_size.x, box_size.y, box_size.z, groundProjectionRadius);
219
+ // TODO: this doesnt take the Camera component nearClipPlane into account
220
+ if (camera instanceof PerspectiveCamera) {
221
+ camera.near = (distance / 100);
222
+ camera.far = boundsMax + distance * 10;
223
+ camera.updateProjectionMatrix();
224
+ }
225
+
226
+ // adjust maxZoom so that the ground projection radius is always inside
227
+ if (groundprojection) {
228
+ options.maxZoom = Math.max(Math.min(options.maxZoom, groundProjectionRadius * 0.5), distance);
229
+ }
230
+ }
231
+
232
+ // ensure we're not clipping out of the current zoom level just because we're fitting
233
+ if (options.currentZoom !== undefined) {
234
+ if (options.currentZoom < options.minZoom) options.minZoom = options.currentZoom * 0.9;
235
+ if (options.currentZoom > options.maxZoom) options.maxZoom = options.currentZoom * 1.1;
236
+ }
237
+
238
+ const direction = center.clone();
239
+ if (options.fitDirection) {
240
+ direction.sub(new Vector3().copy(options.fitDirection).multiplyScalar(1_000_000));
241
+ }
242
+ else {
243
+ direction.sub(camera.worldPosition);
244
+ }
245
+ if (centerCamera === "y")
246
+ direction.y = 0;
247
+ direction.normalize();
248
+ direction.multiplyScalar(distance);
249
+ if (centerCamera === "y")
250
+ direction.y += -verticalOffset * 4 * distance;
251
+
252
+ let cameraLocalPosition = center.clone().sub(direction);
253
+ if (options.cameraOffset) {
254
+ if (options.cameraOffset.x !== undefined) cameraLocalPosition.x += options.cameraOffset.x;
255
+ if (options.cameraOffset.y !== undefined) cameraLocalPosition.y += options.cameraOffset.y;
256
+ if (options.cameraOffset.z !== undefined) cameraLocalPosition.z += options.cameraOffset.z;
257
+ }
258
+ if (options.relativeCameraOffset) {
259
+ if (options.relativeCameraOffset.x !== undefined) cameraLocalPosition.x += options.relativeCameraOffset.x * size.x;
260
+ if (options.relativeCameraOffset.y !== undefined) cameraLocalPosition.y += options.relativeCameraOffset.y * size.y;
261
+ if (options.relativeCameraOffset.z !== undefined) cameraLocalPosition.z += options.relativeCameraOffset.z * size.z;
262
+ }
263
+ if (camera.parent) {
264
+ cameraLocalPosition = camera.parent.worldToLocal(cameraLocalPosition);
265
+ }
266
+ // this.setCameraTargetPosition(cameraLocalPosition, immediate);
267
+
268
+ if (options.debug) {
269
+ Gizmos.DrawWireBox3(box, 0xffff33, 10);
270
+ Gizmos.DrawWireBox3(boxCopy, 0x00ff00, 10);
271
+ }
272
+
273
+ if (options.autoApply) {
274
+ camera.position.copy(cameraLocalPosition);
275
+ camera.lookAt(lookAt);
276
+ if (fov > 0 && camera instanceof PerspectiveCamera) {
277
+ camera.fov = fov;
278
+ camera.updateProjectionMatrix();
279
+ }
280
+ }
281
+
282
+ return {
283
+ camera: camera,
284
+ position: cameraLocalPosition,
285
+ lookAt: lookAt,
286
+ fov: options.fov,
287
+ }
288
+ }
@@ -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;
@@ -592,7 +592,7 @@ namespace NEMeshBVH {
592
592
  const timeSinceLastUpdate = now - skinnedMesh.staticGeometryLastUpdate!;
593
593
  const interval = skinnedMesh.autoUpdateMeshBvhInterval ?? 100;
594
594
  if (skinnedMeshBVHNeedsUpdate || timeSinceLastUpdate > interval) {
595
- if(debugPhysics) console.warn(`Physics: updating skinned mesh bvh for ${mesh.name} after ${timeSinceLastUpdate.toFixed(2)}ms`);
595
+ if (debugPhysics) console.warn(`Physics: updating skinned mesh bvh for ${mesh.name} after ${timeSinceLastUpdate.toFixed(2)}ms`);
596
596
  skinnedMesh.bvhNeedsUpdate = false;
597
597
  skinnedMesh.staticGeometryLastUpdate = now;
598
598
  skinnedMesh.staticGenerator?.generate(skinnedMesh.staticGeometry);
@@ -709,8 +709,11 @@ namespace NEMeshBVH {
709
709
  else {
710
710
  if (debugPhysics) console.warn("No bounds tree found for mesh", mesh.name, { workerTask: geom[workerTaskSymbol], hasAcceleratedRaycast: _acceleratedRaycast != null });
711
711
  if (options.allowSlowRaycastFallback === false) {
712
- if (debugPhysics) console.warn("Skipping raycast because no bounds tree is available and allowSlowRaycastFallback is false");
713
- return false;
712
+ const vertices = geom.getAttribute("position")?.array?.length ?? 0;
713
+ if (vertices > 2000) {
714
+ if (debugPhysics) console.warn("Skipping raycast because no bounds tree is available and allowSlowRaycastFallback is false");
715
+ return false;
716
+ }
714
717
  }
715
718
  }
716
719
  const prevFirstHitOnly = raycaster.firstHitOnly;
@@ -158,8 +158,18 @@ export class EngineLoadingView implements ILoadingViewHandler {
158
158
  private onDoneLoading() {
159
159
  if (this._loadingElement) {
160
160
  if (debug) console.log("Hiding loading element");
161
- this._loadingElement.style.display = "none";
162
- this._loadingElement.remove();
161
+ // animate alpha to 0
162
+ const element = this._loadingElement;
163
+ element.animate([
164
+ { opacity: 1 },
165
+ { opacity: 0 }
166
+ ], {
167
+ duration: 200,
168
+ easing: 'ease-in-out',
169
+ }).addEventListener('finish', () => {
170
+ element.style.display = "none";
171
+ element.remove();
172
+ });
163
173
  }
164
174
  if (this._progressLoop)
165
175
  clearInterval(this._progressLoop);
@@ -190,6 +200,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
190
200
  loadingStyle = "light";
191
201
  }
192
202
 
203
+
193
204
  const hasLicense = hasProLicense();
194
205
  if (!existing) {
195
206
  this._loadingElement.style.position = "absolute";
@@ -197,6 +208,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
197
208
  this._loadingElement.style.height = "100%";
198
209
  this._loadingElement.style.left = "0";
199
210
  this._loadingElement.style.top = "0";
211
+ this._loadingElement.style.overflow = "hidden";
200
212
  const loadingBackgroundColor = this._element.getAttribute("loading-background");
201
213
  if (loadingBackgroundColor) {
202
214
  this._loadingElement.style.background = loadingBackgroundColor;
@@ -227,24 +239,48 @@ export class EngineLoadingView implements ILoadingViewHandler {
227
239
  }
228
240
 
229
241
  const content = document.createElement("div");
242
+ content.style.cssText = `
243
+ position: relative;
244
+ display: flex;
245
+ flex-direction: column;
246
+ align-items: center;
247
+ justify-content: center;
248
+ width: 100%;
249
+ height: 100%;
250
+ pointer-events: none;
251
+ `
230
252
  this._loadingElement.appendChild(content);
231
253
 
254
+ const poster = this._element.getAttribute("poster");
255
+ if (poster !== null && poster !== "0") {
256
+ const backgroundImage = document.createElement("div");
257
+ const backgroundBlur = poster?.length ? "0px" : "50px";
258
+ backgroundImage.style.cssText = `
259
+ position: absolute;
260
+ left: 0;
261
+ top: 0;
262
+ bottom: 0;
263
+ right: 0;
264
+ z-index: -1;
265
+ overflow: hidden;
266
+
267
+ margin: -${backgroundBlur};
268
+ background: url('${poster?.length ? poster : "/include/poster.webp"}') center center no-repeat;
269
+ background-size: cover;
270
+ filter: blur(${backgroundBlur});
271
+ `;
272
+ this._loadingElement.appendChild(backgroundImage);
273
+ }
274
+
232
275
  const logo = document.createElement("img");
233
- const logoSize = 120;
234
- logo.style.width = `${logoSize}px`;
235
- logo.style.height = `${logoSize}px`;
236
- logo.style.paddingTop = "20px";
237
- logo.style.paddingBottom = "10px";
238
- logo.style.margin = "0px";
276
+ const logoWidth = "80%";
277
+ const logoHeight = "15%";
278
+ const logoDelay = ".2s";
239
279
  logo.style.userSelect = "none";
240
280
  logo.style.objectFit = "contain";
241
- logo.style.transition = "transform 1.5s ease-out, opacity .3s ease-in-out";
242
281
  logo.style.transform = "translateY(30px)";
243
- logo.style.opacity = "0.05";
244
- setTimeout(() => {
245
- logo.style.opacity = "1";
246
- logo.style.transform = "translateY(0px)";
247
- }, 1);
282
+ logo.style.opacity = "0.0000001";
283
+ logo.style.transition = `transform 1s ease-out ${logoDelay}, opacity .3s ease-in-out ${logoDelay}`;
248
284
  logo.src = needleLogoOnlySVG;
249
285
  let isUsingCustomLogo = false;
250
286
  if (hasLicense && this._element) {
@@ -252,8 +288,15 @@ export class EngineLoadingView implements ILoadingViewHandler {
252
288
  if (customLogo) {
253
289
  isUsingCustomLogo = true;
254
290
  logo.src = customLogo;
291
+ setTimeout(() => {
292
+ logo.style.opacity = "1";
293
+ logo.style.transform = "translateY(0px)";
294
+ }, 1);
255
295
  }
256
296
  }
297
+
298
+ logo.style.width = `${logoWidth}`;
299
+ logo.style.height = `min(1000px, max(${logoHeight}, 50px))`;
257
300
  content.appendChild(logo);
258
301
 
259
302
  const details = document.createElement("div");
@@ -273,18 +316,14 @@ export class EngineLoadingView implements ILoadingViewHandler {
273
316
  const maxWidth = 100;
274
317
  loadingBarContainer.style.display = "flex";
275
318
  loadingBarContainer.style.width = maxWidth + "%";
276
- loadingBarContainer.style.height = "3px";
319
+ loadingBarContainer.style.height = "5px";
277
320
  loadingBarContainer.style.position = "absolute";
278
321
  loadingBarContainer.style.left = "0";
279
- loadingBarContainer.style.bottom = "0px";
322
+ loadingBarContainer.style.top = "0px";
280
323
  loadingBarContainer.style.opacity = "0";
281
- loadingBarContainer.style.transition = "opacity 1s ease-in-out 2s";
324
+ loadingBarContainer.style.transition = "opacity 1s ease-in-out";
325
+ loadingBarContainer.style.backgroundColor = "rgba(240,240,240,.5)"
282
326
  setTimeout(() => { loadingBarContainer.style.opacity = "1"; }, 1);
283
- if (loadingStyle === "light")
284
- loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
285
- else
286
- loadingBarContainer.style.backgroundColor = "rgba(255,255,255,.2)"
287
- // loadingBarContainer.style.alignItems = "center";
288
327
 
289
328
  this._loadingElement.appendChild(loadingBarContainer);
290
329
 
@@ -294,9 +333,9 @@ export class EngineLoadingView implements ILoadingViewHandler {
294
333
  const getGradientPos = function (t: number): string {
295
334
  return Mathf.lerp(0, maxWidth, t) + "%";
296
335
  }
297
- this._loadingBar.style.background = "#66A22F";
298
- // `linear-gradient(90deg, #204f49 ${getGradientPos(0)}, #0BA398 ${getGradientPos(.3)}, #66A22F ${getGradientPos(.6)}, #D7DB0A ${getGradientPos(1)})`;
336
+ // `linear-gradient(90deg, #204f49 ${getGradientPos(0)}, #0BA398 ${getGradientPos(.3)}, #66A22F ${getGradientPos(.6)}, #D7DB0A ${getGradientPos(1)})`;
299
337
  this._loadingBar.style.backgroundAttachment = "fixed";
338
+ this._loadingBar.style.background = "#c4c4c4ab";
300
339
  this._loadingBar.style.width = "0%";
301
340
  this._loadingBar.style.height = "100%";
302
341
  if (hasLicense && this._element) {
@@ -85,12 +85,17 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
85
85
  * <needle-engine></needle-engine>
86
86
  * @returns {boolean | null} if the attribute is not set it returns null
87
87
  */
88
- public get cameraControls(): boolean | null {
88
+ get cameraControls(): boolean | null {
89
89
  const attr = this.getAttribute("camera-controls") as NeedleEngineAttributes["camera-controls"] | ({} & string)
90
90
  if (attr == null) return null;
91
91
  if (attr === null || attr === "False" || attr === "false" || attr === "0" || attr === "none") return false;
92
92
  return true;
93
93
  }
94
+ set cameraControls(value: boolean | null) {
95
+ if (value === null) this.removeAttribute("camera-controls");
96
+ else this.setAttribute("camera-controls", value ? "true" : "false");
97
+ }
98
+
94
99
 
95
100
  /**
96
101
  * Get the current context for this web component instance. The context is created when the src attribute is set and the loading has finished.
@@ -276,7 +276,7 @@ export class AnimatorController {
276
276
  * @returns The found state or null if not found
277
277
  */
278
278
  FindState(name: string | number | undefined | null): State | null { return this.findState(name); }
279
-
279
+
280
280
  /**
281
281
  * Finds an animation state by name or hash.
282
282
  *
@@ -321,6 +321,25 @@ export class AnimatorController {
321
321
  return action;
322
322
  }
323
323
 
324
+ // addState(state: State, layerIndex: number = 0) {
325
+ // if (!this.model) throw new Error("AnimatorController model is missing");
326
+ // if (layerIndex < 0 || layerIndex >= this.model.layers.length) {
327
+ // throw new Error(`Invalid layer index: ${layerIndex}`);
328
+ // }
329
+ // const layer = this.model.layers[layerIndex];
330
+ // if (!layer.stateMachine) {
331
+ // layer.stateMachine = { states: [], defaultState: 0 };
332
+ // }
333
+ // if (!layer.stateMachine.states) {
334
+ // layer.stateMachine.states = [];
335
+ // }
336
+ // if (state.hash === undefined) {
337
+ // state.hash = stringToHash(state.name || "state" + layer.stateMachine.states.length);
338
+ // }
339
+
340
+ // }
341
+
342
+
324
343
  /**
325
344
  * The normalized time (0-1) to start playing the first state at.
326
345
  * This affects the initial state when the animator is first enabled.
@@ -331,7 +350,7 @@ export class AnimatorController {
331
350
  * The Animator component this controller is bound to.
332
351
  */
333
352
  animator?: Animator;
334
-
353
+
335
354
  /**
336
355
  * The data model describing the animation states and transitions.
337
356
  */
@@ -117,16 +117,15 @@ function createDefaultCameraControls(context: IContext, cam?: ICamera) {
117
117
  if (cameraObject) {
118
118
  const orbit = getOrAddComponent(cameraObject, OrbitControls) as OrbitControls;
119
119
  orbit.sourceId = cam?.sourceId ?? "unknown";
120
+ // /enable auto-rotate if the auto-rotate attribute is provided
120
121
  const autoRotate = context.domElement.getAttribute("auto-rotate");
121
- orbit.autoRotate = autoRotate !== undefined && autoRotate !== null && (autoRotate != "0" && autoRotate?.toLowerCase() != "false");
122
- orbit.autoRotateSpeed = 0.5;
123
- orbit.autoFit = true;
124
- if (orbit.autoRotate && autoRotate) {
125
- const autoRotateValue = parseFloat(autoRotate);
126
- if (!isNaN(autoRotateValue)) {
127
- orbit.autoRotateSpeed = autoRotateValue;
128
- }
129
- }
122
+ orbit.autoRotate = autoRotate != "0" && autoRotate?.toLowerCase() != "false";
123
+ const autoRotateSpeed = Number.parseFloat(autoRotate || ".5");
124
+ orbit.autoRotateSpeed = !isNaN(autoRotateSpeed) ? autoRotateSpeed : .5;
125
+ console.log("Auto-rotate", orbit.autoRotate, "speed:", orbit.autoRotateSpeed);
126
+ const autoFit = context.domElement.getAttribute("auto-fit");
127
+ orbit.autoFit = autoFit !== "0" && autoFit?.toLowerCase() != "false";
128
+ orbit.autoTarget = true;
130
129
  }
131
130
  else {
132
131
  console.warn("Missing camera object, can not add orbit controls")