Needle Engine

Changes between version 3.34.2-alpha.2 and 3.34.3-alpha
Files changed (19) hide show
  1. plugins/vite/asap.js +1 -1
  2. src/engine-components/ui/BaseUIComponent.ts +5 -3
  3. src/engine-components/postprocessing/Effects/Bloom.ts +16 -2
  4. src/engine-components/Component.ts +65 -57
  5. src/engine-components/DragControls.ts +4 -0
  6. src/engine/engine_application.ts +1 -0
  7. src/engine/engine_components.ts +32 -24
  8. src/engine/engine_context.ts +28 -3
  9. src/engine/engine_gizmos.ts +4 -0
  10. src/engine/engine_input.ts +12 -2
  11. src/engine/engine_types.ts +17 -4
  12. src/engine-components/ui/EventSystem.ts +3 -1
  13. src/engine-components/ui/Graphic.ts +19 -17
  14. src/engine/extensions/NEEDLE_lighting_settings.ts +2 -2
  15. src/engine-components/js-extensions/Object3D.ts +14 -12
  16. src/engine-components/ui/PointerEvents.ts +3 -1
  17. src/engine-components/Renderer.ts +6 -1
  18. src/engine-components/postprocessing/Volume.ts +6 -1
  19. src/engine-components/postprocessing/VolumeParameter.ts +13 -1
plugins/vite/asap.js CHANGED
@@ -48,7 +48,7 @@
48
48
  let code = readFileSync(mainTsFilePath, 'utf-8');
49
49
  if (code.includes('import \"@needle-tools/engine\"')) {
50
50
  console.log("Change main.ts and replace needle engine import with async import");
51
- code = code.replace(/import \"@needle-tools\/engine\"/g, '/* async import */ import("@needle-tools/engine")');
51
+ code = code.replace(/import \"@needle-tools\/engine\"/g, 'import("@needle-tools/engine") /* async import of needle engine */');
52
52
  writeFileSync(mainTsFilePath, code);
53
53
  }
54
54
  }
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import * as ThreeMeshUI from 'three-mesh-ui';
4
4
 
5
5
  import { showGizmos } from '../../engine/engine_default_parameters.js';
6
+ import { ComponentInit } from '../../engine/engine_types.js';
6
7
  import { getParam } from '../../engine/engine_utils.js';
7
8
  import { Behaviour, GameObject } from "../Component.js";
8
9
  import { EventSystem } from "./EventSystem.js";
@@ -78,11 +79,12 @@
78
79
  // private _intermediate?: Object3D;
79
80
  protected _parentComponent?: BaseUIComponent | null = undefined;
80
81
 
81
- __internalNewInstanceCreated() {
82
- super.__internalNewInstanceCreated();
82
+ __internalNewInstanceCreated(args: ComponentInit<this>) {
83
+ super.__internalNewInstanceCreated(args);
83
84
  this.shadowComponent = null;
84
85
  this._root = undefined;
85
86
  this._parentComponent = undefined;
87
+ return this;
86
88
  }
87
89
 
88
90
  onEnable() {
@@ -146,7 +148,7 @@
146
148
  if (needsUpdate)
147
149
  ThreeMeshUI.update();
148
150
 
149
- if(debug) console.log(this.shadowComponent)
151
+ if (debug) console.log(this.shadowComponent)
150
152
  }
151
153
 
152
154
  protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
src/engine-components/postprocessing/Effects/Bloom.ts CHANGED
@@ -7,6 +7,9 @@
7
7
 
8
8
  export class Bloom extends PostProcessingEffect {
9
9
 
10
+ /** Whether to use selective bloom by default */
11
+ static useSelectiveBloom = false;
12
+
10
13
  get typeName() {
11
14
  return "Bloom";
12
15
  }
@@ -18,7 +21,7 @@
18
21
  @serializable(VolumeParameter)
19
22
  scatter!: VolumeParameter;
20
23
 
21
- selectiveBloom = true;
24
+ selectiveBloom?: boolean;
22
25
 
23
26
  init() {
24
27
  this.threshold.defaultValue = 1;
@@ -39,6 +42,11 @@
39
42
 
40
43
  onCreateEffect() {
41
44
  let bloom: BloomEffect;
45
+
46
+ if (this.selectiveBloom == undefined) {
47
+ this.selectiveBloom = Bloom.useSelectiveBloom;
48
+ }
49
+
42
50
  if (this.selectiveBloom) {
43
51
  // https://github.com/pmndrs/postprocessing/blob/64d2829f014cfec97a46bf3c109f3abc55af0715/demo/src/demos/BloomDemo.js#L265
44
52
  const selectiveBloom = bloom = new SelectiveBloomEffect(this.context.scene, this.context.mainCamera!, {
@@ -51,7 +59,13 @@
51
59
  selectiveBloom.inverted = true;
52
60
  }
53
61
  else {
54
- bloom = new BloomEffect();
62
+ bloom = new BloomEffect({
63
+ blendFunction: BlendFunction.ADD,
64
+ mipmapBlur: true,
65
+ luminanceThreshold: this.threshold.value,
66
+ luminanceSmoothing: this.scatter.value,
67
+ intensity: this.intensity.value,
68
+ });
55
69
  }
56
70
 
57
71
 
src/engine-components/Component.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment } from "../engine/debug/index.js";
4
- import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
4
+ import { addComponent, addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../engine/engine_components.js";
5
5
  import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
6
6
  import { destroy, findByGuid, foreachComponent, HideFlags, type IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
7
7
  import * as main from "../engine/engine_mainloop_utils.js";
8
8
  import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
9
9
  import { Context, FrameEvent } from "../engine/engine_setup.js";
10
10
  import * as threeutils from "../engine/engine_three_utils.js";
11
- import type { Collision, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
11
+ import type { Collision, ComponentInit, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
12
12
  import type { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
13
13
  import { type IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
14
14
 
@@ -23,11 +23,47 @@
23
23
 
24
24
  export abstract class GameObject extends Object3D implements Object3D, IGameObject {
25
25
 
26
+ // these are implemented via threejs object extensions
27
+ abstract activeSelf: boolean;
28
+
29
+ // The actual implementation / prototype of threejs is modified in js-extensions/Object3D
30
+ abstract get transform(): GameObject;
31
+
32
+ /** @deprecated use `addComponent` */
33
+ abstract addNewComponent<T extends IComponent>(type: ConstructorConcrete<T>, init?: ComponentInit<T>): T;
34
+ /** creates a new component on this gameObject */
35
+ abstract addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>): T;
36
+ abstract removeComponent<T extends IComponent>(comp: T): T;
37
+ abstract getOrAddComponent<T>(typeName: ConstructorConcrete<T> | null): T;
38
+ abstract getComponent<T>(type: Constructor<T>): T | null;
39
+ abstract getComponents<T>(type: Constructor<T>, arr?: T[]): Array<T>;
40
+ abstract getComponentInChildren<T>(type: Constructor<T>): T | null;
41
+ abstract getComponentsInChildren<T>(type: Constructor<T>, arr?: T[]): Array<T>;
42
+ abstract getComponentInParent<T>(type: Constructor<T>): T | null;
43
+ abstract getComponentsInParent<T>(type: Constructor<T>, arr?: T[]): Array<T>;
44
+
45
+
46
+ abstract get worldPosition(): Vector3
47
+ abstract set worldPosition(val: Vector3);
48
+ abstract set worldQuaternion(val: Quaternion);
49
+ abstract get worldQuaternion(): Quaternion;
50
+ abstract set worldRotation(val: Vector3);
51
+ abstract get worldRotation(): Vector3;
52
+ abstract set worldScale(val: Vector3);
53
+ abstract get worldScale(): Vector3;
54
+
55
+ abstract get worldForward(): Vector3;
56
+ abstract get worldRight(): Vector3;
57
+ abstract get worldUp(): Vector3;
58
+
26
59
  guid: string | undefined;
27
60
 
28
61
  // Added to the threejs Object3D prototype
29
62
  abstract destroy();
30
63
 
64
+
65
+
66
+
31
67
  public static isDestroyed(go: Object3D): boolean {
32
68
  return isDestroyed(go);
33
69
  }
@@ -166,38 +202,31 @@
166
202
  }, children);
167
203
  }
168
204
 
205
+ /** @deprecated Use `addComponent` */
206
+ public static addNewComponent<T extends IComponent>(go: IGameObject | Object3D, type: T | ConstructorConcrete<T>, init?: ComponentInit<T>, callAwake: boolean = true): T {
207
+ return addComponent(go, type, init, { callAwake });
208
+ }
209
+
169
210
  /**
170
- * Adds a new component to the provided object
211
+ * Add a new component (or move an existing component) to the provided object
171
212
  * @param go object to add the component to
172
- * @param type type of the component to add
213
+ * @param instanceOrType if an instance is provided it will be moved to the new object, if a type is provided a new instance will be created and moved to the new object
214
+ * @param init optional init object to initialize the component with
173
215
  * @param callAwake if true, the component will be added and awake will be called immediately
174
216
  */
175
- public static addNewComponent<T>(go: IGameObject | Object3D, type: ConstructorConcrete<T>, callAwake: boolean = true): T {
176
- const instance = new type();
177
- //@ts-ignore
178
- addNewComponent(go, instance, callAwake);
179
- return instance
217
+ public static addComponent<T extends IComponent>(go: IGameObject | Object3D, instanceOrType: T | ConstructorConcrete<T>, init?: ComponentInit<T>, opts?: { callAwake: boolean }): T {
218
+ return addComponent(go, instanceOrType, init, opts);
180
219
  }
181
220
 
182
221
  /**
183
222
  * Moves a component to a new object
184
- * BEWARE: this does MOVE a component. If you want to add a new component use `addNewComponent`
185
223
  * @param go component to move the component to
186
224
  * @param instance component to move to the GO
187
225
  */
188
- public static addComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>) {
189
- return this.moveComponent(go, instance);
226
+ public static moveComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>): T {
227
+ return addComponent(go, instance);
190
228
  }
191
229
 
192
- /**
193
- * Moves a component to a new object
194
- * @param go component to move the component to
195
- * @param instance component to move to the GO
196
- */
197
- public static moveComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>) {
198
- return moveComponentInstance(go, instance);
199
- }
200
-
201
230
  /** Removes a component from its object
202
231
  * @param instance component to remove
203
232
  */
@@ -270,39 +299,6 @@
270
299
  }
271
300
  }
272
301
  }
273
-
274
- // these are implemented via threejs object extensions
275
- abstract activeSelf: boolean;
276
-
277
- // The actual implementation / prototype of threejs is modified in js-extensions/Object3D
278
- abstract get transform(): GameObject;
279
-
280
- /** creates a new component on this gameObject */
281
- abstract addNewComponent<T>(type: ConstructorConcrete<T>): T | null;
282
- /** adds an existing component to this gameObject */
283
- abstract addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>): void;
284
- abstract removeComponent(comp: Component): Component;
285
- abstract getOrAddComponent<T>(typeName: ConstructorConcrete<T> | null): T;
286
- abstract getComponent<T>(type: Constructor<T>): T | null;
287
- abstract getComponents<T>(type: Constructor<T>, arr?: T[]): Array<T>;
288
- abstract getComponentInChildren<T>(type: Constructor<T>): T | null;
289
- abstract getComponentsInChildren<T>(type: Constructor<T>, arr?: T[]): Array<T>;
290
- abstract getComponentInParent<T>(type: Constructor<T>): T | null;
291
- abstract getComponentsInParent<T>(type: Constructor<T>, arr?: T[]): Array<T>;
292
-
293
-
294
- abstract get worldPosition(): Vector3
295
- abstract set worldPosition(val: Vector3);
296
- abstract set worldQuaternion(val: Quaternion);
297
- abstract get worldQuaternion(): Quaternion;
298
- abstract set worldRotation(val: Vector3);
299
- abstract get worldRotation(): Vector3;
300
- abstract set worldScale(val: Vector3);
301
- abstract get worldScale(): Vector3;
302
-
303
- abstract get worldForward(): Vector3;
304
- abstract get worldRight(): Vector3;
305
- abstract get worldUp(): Vector3;
306
302
  }
307
303
 
308
304
 
@@ -551,24 +547,36 @@
551
547
 
552
548
 
553
549
  /** @internal */
554
- constructor() {
550
+ constructor(init?: ComponentInit<Component>) {
555
551
  this.__didAwake = false;
556
552
  this.__didStart = false;
557
553
  this.__didEnable = false;
558
554
  this.__isEnabled = undefined;
559
555
  this.__destroyed = false;
556
+ this._internalInit(init as ComponentInit<this>);
560
557
  }
561
558
 
562
-
563
559
  /** @internal */
564
- __internalNewInstanceCreated() {
560
+ __internalNewInstanceCreated(init?: ComponentInit<this>): this {
565
561
  this.__didAwake = false;
566
562
  this.__didStart = false;
567
563
  this.__didEnable = false;
568
564
  this.__isEnabled = undefined;
569
565
  this.__destroyed = false;
566
+ this._internalInit(init);
567
+ return this;
570
568
  }
571
569
 
570
+ _internalInit(init?: ComponentInit<this>) {
571
+ if (typeof init === "object") {
572
+ for (const key of Object.keys(init)) {
573
+ const value = init[key];
574
+ // we don't want to allow overriding functions via init
575
+ if (typeof value === "function") continue;
576
+ (this as any)[key] = value;
577
+ }
578
+ }
579
+ }
572
580
 
573
581
  /** @internal */
574
582
  __internalAwake() {
src/engine-components/DragControls.ts CHANGED
@@ -767,6 +767,9 @@
767
767
  const localP = this.gameObject.position.clone();
768
768
  const localQ = this.gameObject.quaternion.clone();
769
769
  const localS = this.gameObject.scale.clone();
770
+ // save the original matrix world (because if some other script is doing a raycast at the same moment the matrix will not be correct anymore....)
771
+ const matrixWorld = this.gameObject.matrixWorld.clone();
772
+
770
773
  if (p) p.remove(this.gameObject);
771
774
  this.gameObject.position.set(0, 0, 0);
772
775
  this.gameObject.quaternion.set(0, 0, 0, 1);
@@ -791,6 +794,7 @@
791
794
  this.gameObject.position.copy(localP);
792
795
  this.gameObject.quaternion.copy(localQ);
793
796
  this.gameObject.scale.copy(localS);
797
+ this.gameObject.matrixWorld.copy(matrixWorld);
794
798
 
795
799
  // surface snapping
796
800
  this._draggedOverObject = null;
src/engine/engine_application.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  document.addEventListener('click', onUserInteraction);
23
23
  document.addEventListener('dragstart', onUserInteraction);
24
24
  document.addEventListener('touchstart', onUserInteraction);
25
+ document.addEventListener('keydown', onUserInteraction);
25
26
  NeedleXRSession.onXRSessionStart(() => {
26
27
  onUserInteraction();
27
28
  })
src/engine/engine_components.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
7
7
  import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
8
8
  import { Context, registerComponent } from "./engine_setup.js";
9
- import type { Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
9
+ import type { ComponentInit, Constructor, ConstructorConcrete, IComponent, IGameObject } from "./engine_types.js";
10
10
  import { getParam } from "./engine_utils.js";
11
11
 
12
12
 
@@ -24,29 +24,29 @@
24
24
  }
25
25
 
26
26
 
27
- export function removeComponent(go: Object3D, componentInstance: IComponent) {
28
- if (!go) return;
29
- if (!go.userData.components) return;
27
+ export function removeComponent<T extends IComponent>(go: Object3D, componentInstance: T): T {
28
+ if (!go) return componentInstance;
29
+ if (!go.userData.components) return componentInstance;
30
30
  const index = go.userData.components.indexOf(componentInstance);
31
- if (index < 0) return;
31
+ if (index < 0) return componentInstance;
32
32
 
33
33
  ComponentLifecycleEvents.dispatchComponentLifecycleEvent(ComponentEvents.Removing, componentInstance);
34
34
 
35
35
  //@ts-ignore
36
36
  componentInstance.gameObject = null;
37
37
  go.userData.components.splice(index, 1);
38
+ return componentInstance;
38
39
  }
39
40
 
40
- export function getOrAddComponent<T extends IComponent>(go: Object3D, typeName: ConstructorConcrete<T>): T {
41
+ export function getOrAddComponent<T extends IComponent>(go: Object3D, typeName: ConstructorConcrete<T>, init?: ComponentInit<T>): T {
41
42
  const comp = getComponent(go, typeName);
42
43
  if (comp) return comp;
43
- const newInstance = new typeName();
44
- return addNewComponent(go, newInstance) as unknown as T;
44
+ return addComponent(go, typeName, init);
45
45
  }
46
46
 
47
47
  const idProvider = new InstantiateIdProvider("addComponentIdProvider");
48
48
 
49
- export function addNewComponent<T extends IComponent>(obj: Object3D, componentInstance: T, callAwake = true): IComponent {
49
+ export function addNewComponent<T extends IComponent>(obj: Object3D, componentInstance: T, callAwake = true): T {
50
50
  if (!obj) {
51
51
  new Error("Can not add componet to null object");
52
52
  }
@@ -75,10 +75,14 @@
75
75
  return componentInstance;
76
76
  }
77
77
 
78
- export function moveComponentInstance(obj: Object3D, componentInstance: IComponent | ConstructorConcrete<IComponent>) {
78
+ export function addComponent<T extends IComponent>(obj: Object3D, componentInstance: T | ConstructorConcrete<T>, init?: ComponentInit<T>, opts?: { callAwake: boolean }): T {
79
79
 
80
80
  if (typeof componentInstance === "function") {
81
- return addNewComponent(obj, new componentInstance());
81
+ const instance = new componentInstance();
82
+ if (init) instance.__internalNewInstanceCreated(init);
83
+ let callAwake = true;
84
+ if (opts?.callAwake != undefined) callAwake = opts.callAwake;
85
+ return addNewComponent<T>(obj, instance, callAwake);
82
86
  }
83
87
 
84
88
  if (componentInstance.destroyed) {
@@ -103,6 +107,7 @@
103
107
  if (componentInstance.guid === undefined || componentInstance.guid === "invalid") {
104
108
  componentInstance.guid = idProvider.generateUUID();
105
109
  }
110
+ if(init) componentInstance._internalInit(init);
106
111
  registerComponent(componentInstance);
107
112
  return componentInstance;
108
113
  }
@@ -126,7 +131,7 @@
126
131
 
127
132
  let didWarnAboutComponentAccess: boolean = false;
128
133
 
129
- function onGetComponent<T>(obj: Object3D | null | undefined, componentType: Constructor<T>, arr?: T[]) {
134
+ function onGetComponent<T>(obj: Object3D | null | undefined, componentType: Constructor<T>, arr?: T[]): T | T[] | null {
130
135
  if (obj === null || obj === undefined) return null;
131
136
  if (!obj.isObject3D) {
132
137
  console.error("Object is not object3D");
@@ -164,21 +169,24 @@
164
169
  return arr;
165
170
  }
166
171
 
167
- export function getComponent<T>(obj: Object3D, componentType: Constructor<T>) {
168
- return onGetComponent(obj, componentType);
172
+ export function getComponent<T extends IComponent>(obj: Object3D, componentType: Constructor<T>): T | null {
173
+ const result = onGetComponent(obj, componentType);
174
+ if (!result) return null;
175
+ if (Array.isArray(result)) return result[0];
176
+ return result;
169
177
  }
170
178
 
171
- export function getComponents<T>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null, clearArray: boolean = true): T[] {
179
+ export function getComponents<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null, clearArray: boolean = true): T[] {
172
180
  if (!arr) arr = [];
173
181
  if (clearArray) arr.length = 0;
174
182
  onGetComponent(obj, componentType, arr);
175
183
  return arr;
176
184
  }
177
185
 
178
- export function getComponentInChildren<T>(obj: Object3D, componentType: Constructor<T>, includeInactive?: boolean) {
179
- const res = getComponent(obj, componentType);
186
+ export function getComponentInChildren<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, includeInactive?: boolean): T | null {
187
+ const res = getComponent(obj, componentType) as IComponent | null;
180
188
  if (includeInactive === false && res?.enabled === false) return null;
181
- if (res) return res;
189
+ if (res) return res as T;
182
190
  for (let i = 0; i < obj?.children?.length; i++) {
183
191
  const res = getComponentInChildren(obj.children[i], componentType);
184
192
  if (res) return res;
@@ -186,7 +194,7 @@
186
194
  return null;
187
195
  }
188
196
 
189
- export function getComponentsInChildren<T>(obj: Object3D, componentType: Constructor<T>, arr?: T[], clearArray: boolean = true) {
197
+ export function getComponentsInChildren<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, arr?: T[], clearArray: boolean = true): T[] | null {
190
198
  if (!arr) arr = [];
191
199
  if (clearArray) arr.length = 0;
192
200
 
@@ -197,7 +205,7 @@
197
205
  return arr;
198
206
  }
199
207
 
200
- export function getComponentInParent<T>(obj: Object3D, componentType: Constructor<T>) {
208
+ export function getComponentInParent<T extends IComponent>(obj: Object3D, componentType: Constructor<T>): T | null {
201
209
  if (!obj) return null;
202
210
  if (Array.isArray(obj)) {
203
211
  for (let i = 0; i < obj.length; i++) {
@@ -215,7 +223,7 @@
215
223
  return null;
216
224
  }
217
225
 
218
- export function getComponentsInParent<T>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null, clearArray: boolean = true): T[] {
226
+ export function getComponentsInParent<T extends IComponent>(obj: Object3D, componentType: Constructor<T>, arr?: T[] | null, clearArray: boolean = true): T[] {
219
227
  if (!arr) arr = [];
220
228
  if (clearArray) arr.length = 0;
221
229
 
@@ -226,7 +234,7 @@
226
234
  return arr;
227
235
  }
228
236
 
229
- export function findObjectOfType<T>(type: Constructor<T>, contextOrScene: Object3D | { scene: Scene }, includeInactive) {
237
+ export function findObjectOfType<T extends IComponent>(type: Constructor<T>, contextOrScene: Object3D | { scene: Scene }, includeInactive): T | null {
230
238
  if (!type) return null;
231
239
  if (!contextOrScene) {
232
240
  contextOrScene = Context.Current;
@@ -244,14 +252,14 @@
244
252
  for (const i in scene.children) {
245
253
  const child = scene.children[i];
246
254
  if (includeInactive === false && child[activeInHierarchyFieldName] === false) continue;
247
- if (child.constructor == type) return child;
255
+ // if (child.constructor == type) return child as T;
248
256
  const res = getComponentInChildren(child, type);
249
257
  if (res) return res;
250
258
  }
251
259
  return null;
252
260
  }
253
261
 
254
- export function findObjectsOfType<T>(type: Constructor<T>, array: T[], contextOrScene): T[] {
262
+ export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array: T[], contextOrScene): T[] {
255
263
  if (!type) return array;
256
264
  if (!array) array = [];
257
265
  array.length = 0;
src/engine/engine_context.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  import { RendererData as SceneLighting } from './engine_scenelighting.js';
28
28
  import { logHierarchy } from './engine_three_utils.js';
29
29
  import { Time } from './engine_time.js';
30
- import type { CoroutineData, GLTF, ICamera, IComponent, IContext, ILight, LoadedGLTF } from "./engine_types.js";
30
+ import type { CoroutineData, GLTF, ICamera, IComponent, IContext, ILight, LoadedGLTF, Vec2 } from "./engine_types.js";
31
31
  import * as utils from "./engine_utils.js";
32
32
  import { delay, getParam } from './engine_utils.js';
33
33
  import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
@@ -452,13 +452,22 @@
452
452
  /** will request a renderer size update the next render call (will call updateSize the next update) */
453
453
  requestSizeUpdate() { this._sizeChanged = true; }
454
454
 
455
+ /** Clamps the renderer max resolution. If undefined the max resolution is not clamped. Default is undefined */
456
+ maxRenderResolution?: Vec2;
457
+
455
458
  /** update the renderer and canvas size */
456
459
  updateSize(force: boolean = false) {
457
460
  if (force || (!this.isManagedExternally && this.renderer.xr?.isPresenting === false)) {
458
461
  this._sizeChanged = false;
459
462
  const scaleFactor = this.resolutionScaleFactor;
460
- const width = this.domWidth * scaleFactor;
461
- const height = this.domHeight * scaleFactor;
463
+ let width = this.domWidth * scaleFactor;
464
+ let height = this.domHeight * scaleFactor;
465
+ if (this.maxRenderResolution) {
466
+ this.maxRenderResolution.x = Math.max(1, this.maxRenderResolution.x);
467
+ width = Math.min(this.maxRenderResolution.x, width);
468
+ this.maxRenderResolution.y = Math.max(1, this.maxRenderResolution.y);
469
+ height = Math.min(this.maxRenderResolution.y, height);
470
+ }
462
471
  const camera = this.mainCamera as PerspectiveCamera;
463
472
  this.updateAspect(camera);
464
473
  this.renderer.setSize(width, height, true);
@@ -738,6 +747,22 @@
738
747
 
739
748
  if (debug) console.log("Creating context", this.name, opts);
740
749
 
750
+ // wait for async imported dependencies to be loaded
751
+ // see https://linear.app/needle/issue/NE-4445
752
+ const dependenciesReady = globalThis["needle:dependencies:ready"];
753
+ if (dependenciesReady instanceof Promise) {
754
+ if (debug) console.log("Waiting for dependencies to be ready");
755
+ await dependenciesReady
756
+ .catch(err => {
757
+ if (debug || isDevEnvironment())
758
+ console.error("Needle Engine dependencies failed to load", err)
759
+ })
760
+ .then(() => {
761
+ if(debug) console.log("Needle Engine dependencies are ready");
762
+ globalThis["needle:dependencies:ready"] = true;
763
+ });
764
+ }
765
+
741
766
  this.clear();
742
767
  // stop the animation loop if its running during creation
743
768
  // since we do not want to start enabling scripts etc before they are deserialized
src/engine/engine_gizmos.ts CHANGED
@@ -97,6 +97,7 @@
97
97
  obj.material["color"].set(color);
98
98
  obj.material["depthTest"] = depthTest;
99
99
  obj.material["depthWrite"] = false;
100
+ obj.material["fog"] = false;
100
101
  }
101
102
 
102
103
  static DrawWireSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
@@ -106,6 +107,7 @@
106
107
  obj.material["color"].set(color);
107
108
  obj.material["depthTest"] = depthTest;
108
109
  obj.material["depthWrite"] = false;
110
+ obj.material["fog"] = false;
109
111
  }
110
112
 
111
113
  static DrawSphere(center: Vec3, radius: number, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
@@ -126,6 +128,7 @@
126
128
  obj.material["depthTest"] = depthTest;
127
129
  obj.material["wireframe"] = true;
128
130
  obj.material["depthWrite"] = false;
131
+ obj.material["fog"] = false;
129
132
  }
130
133
 
131
134
  static DrawWireBox3(box: Box3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
@@ -137,6 +140,7 @@
137
140
  obj.material["depthTest"] = depthTest;
138
141
  obj.material["wireframe"] = true;
139
142
  obj.material["depthWrite"] = false;
143
+ obj.material["fog"] = false;
140
144
  }
141
145
 
142
146
  private static _up = new Vector3(0, 1, 0);
src/engine/engine_input.ts CHANGED
@@ -73,7 +73,17 @@
73
73
  readonly mode: XRTargetRayMode;
74
74
  /** A ray in worldspace for the event.
75
75
  * If the ray is undefined you can also use `space.worldForward` and `space.worldPosition` */
76
- readonly ray?: Ray;
76
+ get ray(): Ray {
77
+ if (!this._ray) {
78
+ this._ray = new Ray(this.space.worldPosition.clone(), this.space.worldForward.clone());
79
+ }
80
+ return this._ray;
81
+ }
82
+ private set ray(value: Ray) { this._ray = value; }
83
+ /**@returns true if this event has a ray. If you access the ray property a ray will automatically created */
84
+ get hasRay() { return this._ray !== undefined; }
85
+ private _ray: Ray | undefined;
86
+
77
87
  /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
78
88
  * E.g. you can access the input world space source position with `space.worldPosition` or world direction with `space.worldForward`
79
89
  */
@@ -118,7 +128,7 @@
118
128
  this.origin = init.origin;
119
129
  this.source = source;
120
130
  this.mode = init.mode;
121
- this.ray = init.ray;
131
+ this._ray = init.ray;
122
132
  this.space = init.device;
123
133
  }
124
134
 
src/engine/engine_types.ts CHANGED
@@ -9,6 +9,17 @@
9
9
  import { CircularBuffer } from "./engine_utils.js";
10
10
  import type { INeedleXRSessionEventReceiver, NeedleXRSession } from "./engine_xr.js";
11
11
 
12
+ /** Removes all properties that match TypesToFilter */
13
+ type FilterTypes<T, TypesToFilter> = { [P in keyof T as T[P] extends TypesToFilter ? never : P]: T[P] };
14
+ /** Removes all undefined functions */
15
+ type NoUndefinedNoFunctions<T> = FilterTypes<T, Function | undefined | null>;
16
+ /* Removes all properties that start with a specific prefix */
17
+ type FilterStartingWith<T, Prefix extends string> = { [P in keyof T as P extends (`${Prefix}${infer _X}`) ? never : P]: T[P] };
18
+ /** Removes all properties that start with an underscore */
19
+ type NoInternals<T> = FilterStartingWith<T, "_">;
20
+ type NoInternalNeedleEngineState<T> = Omit<T, "destroyed" | "gameObject" | "activeAndEnabled" | "context" | "isComponent" | "scene" | "up" | "forward" | "right" | "worldRotation" | "worldEuler" | "worldPosition" | "worldQuaternion">;
21
+ export type ComponentInit<T> = Partial<NoInternalNeedleEngineState<NoInternals<NoUndefinedNoFunctions<T>>>>
22
+
12
23
  export type GLTF = GLTF3 & {
13
24
  // asset: { generator: string, version: string }
14
25
  // animations: AnimationClip[];
@@ -88,11 +99,12 @@
88
99
  get transform(): IGameObject;
89
100
 
90
101
  /** Add a new component to this object. Expects a component type (e.g. `addNewComponent(Animator)`) */
91
- addNewComponent<T>(type: Constructor<T>): T | null;
102
+ addNewComponent<T extends IComponent>(type: Constructor<T>, init?: ComponentInit<T>): T;
92
103
  /** Remove a component from this object. Expected a component instance
93
104
  * @returns the removed component (equal to the passed in component)
94
105
  */
95
- removeComponent(comp: IComponent): IComponent;
106
+ addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>): T;
107
+ removeComponent<T extends IComponent>(comp: T): T;
96
108
  /** Searches for a component type on this object. If no component of the searched type exists yet a new instance will be created and returned */
97
109
  getOrAddComponent<T>(typeName: Constructor<T> | null): T;
98
110
  /** Tries to find a component of a type on this object.
@@ -144,10 +156,11 @@
144
156
  context: any;
145
157
 
146
158
  get activeAndEnabled(): boolean;
147
-
148
159
  /** @internal */
149
- __internalNewInstanceCreated();
160
+ __internalNewInstanceCreated(init?: ComponentInit<any>);
150
161
  /** @internal */
162
+ _internalInit(init?: ComponentInit<any>);
163
+ /** @internal */
151
164
  __internalAwake();
152
165
  /** @internal */
153
166
  __internalStart();
src/engine-components/ui/EventSystem.ts CHANGED
@@ -173,7 +173,7 @@
173
173
 
174
174
  // raycast
175
175
  const options = new RaycastOptions();
176
- if (pointerEvent.ray) {
176
+ if (pointerEvent.hasRay) {
177
177
  options.ray = pointerEvent.ray;
178
178
  }
179
179
  else {
@@ -303,6 +303,7 @@
303
303
 
304
304
  private assignHitInformation(args: PointerEventData, hit?: Intersection) {
305
305
  if (!hit) {
306
+ args.intersection = undefined;
306
307
  args.point = undefined;
307
308
  args.normal = undefined;
308
309
  args.face = undefined;
@@ -310,6 +311,7 @@
310
311
  args.instanceId = undefined;
311
312
  }
312
313
  else {
314
+ args.intersection = hit;
313
315
  args.point = hit.point;
314
316
  args.normal = hit.normal;
315
317
  args.face = hit.face;
src/engine-components/ui/Graphic.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js"
5
5
 
6
6
  import { serializable } from '../../engine/engine_serialization_decorator.js';
7
+ import { ComponentInit } from '../../engine/engine_types.js';
7
8
  import { NEEDLE_progressive } from '../../engine/extensions/NEEDLE_progressive.js';
8
9
  import { GameObject } from '../Component.js';
9
10
  import { RGBAColor } from "../js-extensions/RGBAColor.js"
@@ -89,11 +90,12 @@
89
90
  this.markDirty();
90
91
  }
91
92
 
92
- __internalNewInstanceCreated(): void {
93
- super.__internalNewInstanceCreated();
93
+ __internalNewInstanceCreated(init: ComponentInit<this>): this {
94
+ super.__internalNewInstanceCreated(init);
94
95
  this._rect = null;
95
96
  this.uiObject = null;
96
97
  if (this._color) this._color = this._color.clone();
98
+ return this;
97
99
  }
98
100
 
99
101
  setState(state: string) {
@@ -202,22 +204,22 @@
202
204
  if (tex) {
203
205
  // workaround for https://github.com/needle-tools/needle-engine-support/issues/109
204
206
  // if (tex.colorSpace === SRGBColorSpace || !tex.colorSpace || true) {
205
- if (Graphic.textureCache.has(tex)) {
206
- tex = Graphic.textureCache.get(tex)!;
207
- } else {
208
- if (tex.isRenderTargetTexture) {
209
- // we can not clone the texture if it's a render target
210
- // otherwise it won't be updated anymore in the UI
211
- // TODO: below maskable graphic is flipped but settings a rendertexture results in the texture being upside down.
212
- // we should remove the flip below (scale.y *= -1) but this needs to be tested with all UI components
213
- }
214
- else {
215
- const clone = tex.clone();
216
- clone.colorSpace = LinearSRGBColorSpace;
217
- Graphic.textureCache.set(tex, clone);
218
- tex = clone;
219
- }
207
+ if (Graphic.textureCache.has(tex)) {
208
+ tex = Graphic.textureCache.get(tex)!;
209
+ } else {
210
+ if (tex.isRenderTargetTexture) {
211
+ // we can not clone the texture if it's a render target
212
+ // otherwise it won't be updated anymore in the UI
213
+ // TODO: below maskable graphic is flipped but settings a rendertexture results in the texture being upside down.
214
+ // we should remove the flip below (scale.y *= -1) but this needs to be tested with all UI components
220
215
  }
216
+ else {
217
+ const clone = tex.clone();
218
+ clone.colorSpace = LinearSRGBColorSpace;
219
+ Graphic.textureCache.set(tex, clone);
220
+ tex = clone;
221
+ }
222
+ }
221
223
  // }
222
224
  this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
223
225
  NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId, tex, 0).then(res => {
src/engine/extensions/NEEDLE_lighting_settings.ts CHANGED
@@ -51,14 +51,14 @@
51
51
  if (_result.scene.children.length === 1) {
52
52
  const obj = _result.scene.children[0];
53
53
  // add a component to the root of the scene
54
- settings = GameObject.addNewComponent(obj, SceneLightSettings, false);
54
+ settings = GameObject.addNewComponent(obj, SceneLightSettings, {}, false);
55
55
  }
56
56
  // if the scene already has multiple children we add it as a new object
57
57
  else {
58
58
  const lightSettings = new Object3D();
59
59
  lightSettings.name = "LightSettings " + this.sourceId;
60
60
  _result.scene.add(lightSettings);
61
- settings = GameObject.addNewComponent(lightSettings, SceneLightSettings, false);
61
+ settings = GameObject.addNewComponent(lightSettings, SceneLightSettings, {}, false);
62
62
  }
63
63
  settings.sourceId = this.sourceId;
64
64
  settings.ambientIntensity = ext.ambientIntensity;
src/engine-components/js-extensions/Object3D.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Object3D, Quaternion, Vector3 } from "three";
2
2
  import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";
3
3
 
4
- import { addNewComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../../engine/engine_components.js";
5
- import { destroy,isActiveSelf, setActive } from "../../engine/engine_gameobject.js";
4
+ import { addComponent, addNewComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../../engine/engine_components.js";
5
+ import { destroy, isActiveSelf, setActive } from "../../engine/engine_gameobject.js";
6
6
  import {
7
7
  getTempVector,
8
8
  getWorldPosition,
@@ -12,13 +12,14 @@
12
12
  setWorldPosition,
13
13
  setWorldQuaternion,
14
14
  setWorldRotation,
15
- setWorldScale}
15
+ setWorldScale
16
+ }
16
17
  from "../../engine/engine_three_utils.js";
17
- import type { Constructor, ConstructorConcrete, IComponent as Component,IComponent } from "../../engine/engine_types.js";
18
+ import type { ComponentInit,Constructor, ConstructorConcrete, IComponent as Component, IComponent, IGameObject } from "../../engine/engine_types.js";
18
19
  import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
19
20
 
20
21
 
21
- // used to decorate cloned object3D objects with the same added components defined above
22
+ /** used to decorate cloned object3D objects with the same added components defined above */
22
23
  export function apply(object: Object3D) {
23
24
  if (object && object.isObject3D === true) {
24
25
  applyPrototypeExtensions(object, Object3D);
@@ -37,20 +38,21 @@
37
38
  destroy(this);
38
39
  }
39
40
 
40
- Object3D.prototype["addComponent"] = function <T extends IComponent>(instance: T | ConstructorConcrete<T>) {
41
- return moveComponentInstance(this, instance);
41
+
42
+ Object3D.prototype["addComponent"] = function <T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>) {
43
+ return addComponent(this, comp, init);
42
44
  }
43
45
 
44
- Object3D.prototype["addNewComponent"] = function <T extends Component>(type: ConstructorConcrete<T>) {
45
- return addNewComponent(this, new type());
46
+ Object3D.prototype["addNewComponent"] = function <T extends Component>(type: ConstructorConcrete<T>, init?: ComponentInit<T>) {
47
+ return addComponent(this, type, init);
46
48
  }
47
49
 
48
50
  Object3D.prototype["removeComponent"] = function (inst: Component) {
49
51
  return removeComponent(this, inst);
50
52
  }
51
53
 
52
- Object3D.prototype["getOrAddComponent"] = function <T extends IComponent>(typeName: ConstructorConcrete<T>): T {
53
- return getOrAddComponent<T>(this, typeName);
54
+ Object3D.prototype["getOrAddComponent"] = function <T extends IComponent>(typeName: ConstructorConcrete<T>, init?: ComponentInit<T>): T {
55
+ return getOrAddComponent<T>(this, typeName, init);
54
56
  }
55
57
 
56
58
  Object3D.prototype["getComponent"] = function <T extends IComponent>(type: Constructor<T>) {
@@ -73,7 +75,7 @@
73
75
  return getComponentInParent(this, type);
74
76
  }
75
77
 
76
- Object3D.prototype["getComponentsInParent"] = function <T>(type: Constructor<T>, arr?: []) {
78
+ Object3D.prototype["getComponentsInParent"] = function <T extends IComponent>(type: Constructor<T>, arr?: []) {
77
79
  return getComponentsInParent(this, type, arr);
78
80
  }
79
81
 
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Face, Object3D, Vector3 } from "three";
1
+ import { type Face, Intersection,Object3D, Vector3 } from "three";
2
2
 
3
3
  import { Input, type InputEventNames, NEPointerEvent } from "../../engine/engine_input.js";
4
4
  import type { GamepadButtonName, MouseButtonName } from "../../engine/engine_types.js";
@@ -102,6 +102,8 @@
102
102
  distance?: number;
103
103
  /** The instance ID of an object hit by a raycast (if a instanced object was hit) */
104
104
  instanceId?: number;
105
+ /** The three intersection */
106
+ intersection?: Intersection;
105
107
 
106
108
  isDown: boolean | undefined;
107
109
  isUp: boolean | undefined;
src/engine-components/Renderer.ts CHANGED
@@ -588,6 +588,12 @@
588
588
  return;
589
589
  }
590
590
 
591
+ if (debugRenderer == this.name && this.gameObject instanceof Mesh) {
592
+ this.gameObject.geometry.computeBoundingSphere();
593
+ const tempCenter = getTempVector(this.gameObject.geometry.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld);
594
+ Gizmos.DrawWireSphere(tempCenter, this.gameObject.geometry.boundingSphere.radius, 0x00ddff);
595
+ }
596
+
591
597
  if (this.isMultiMaterialObject(this.gameObject) && this.gameObject.children?.length > 0) {
592
598
  for (const ch of this.gameObject.children) {
593
599
  this.applySettings(ch);
@@ -776,7 +782,6 @@
776
782
  }
777
783
 
778
784
  export class MeshRenderer extends Renderer {
779
-
780
785
  }
781
786
 
782
787
  export class SkinnedMeshRenderer extends MeshRenderer {
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -18,6 +18,11 @@
18
18
  @serializeable(VolumeProfile)
19
19
  sharedProfile?: VolumeProfile;
20
20
 
21
+ /** Currently active postprocessing effects */
22
+ get effects() {
23
+ return this._effects;
24
+ }
25
+
21
26
  private _postprocessing?: PostProcessingHandler;
22
27
  private _effects: PostProcessingEffect[] = [];
23
28
 
@@ -37,7 +42,7 @@
37
42
  }
38
43
  });
39
44
  }
40
-
45
+
41
46
  // ensure the profile is initialized
42
47
  this.sharedProfile?.init();
43
48
  }
src/engine-components/postprocessing/VolumeParameter.ts CHANGED
@@ -63,7 +63,19 @@
63
63
  return;
64
64
 
65
65
  const oldValue = this._value;
66
- if (debug) console.log("VolumeParameter: value changed from", oldValue, "to", val);
66
+ if (debug) {
67
+ let hasChanged = true;
68
+ if (typeof oldValue == "number" && typeof val == "number") {
69
+ const oldFixed = oldValue?.toFixed(4);
70
+ const newFixed = val?.toFixed(4);
71
+ if (oldFixed != newFixed) {
72
+ hasChanged = true;
73
+ }
74
+ else hasChanged = false;
75
+ }
76
+ if (hasChanged)
77
+ console.log("VolumeParameter: value changed from", oldValue, "to", val);
78
+ }
67
79
 
68
80
  if (!this._active && this._defaultValue !== undefined) {
69
81
  // when setting the default value we dont process them (default values are explicitly set from the effect that declares them