@needle-tools/engine 3.9.1-alpha → 3.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 (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/needle-engine.js +1 -1
  3. package/dist/needle-engine.light.js +1 -1
  4. package/dist/needle-engine.light.min.js +1 -1
  5. package/dist/needle-engine.light.umd.cjs +1 -1
  6. package/dist/needle-engine.min.js +1 -1
  7. package/dist/needle-engine.umd.cjs +1 -1
  8. package/lib/engine/engine_element_overlay.d.ts +2 -5
  9. package/lib/engine/engine_element_overlay.js +18 -30
  10. package/lib/engine/engine_element_overlay.js.map +1 -1
  11. package/lib/engine/engine_input.js +10 -5
  12. package/lib/engine/engine_input.js.map +1 -1
  13. package/lib/engine/engine_mainloop_utils.js +6 -1
  14. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  15. package/lib/engine/engine_networking_auto.d.ts +4 -1
  16. package/lib/engine/engine_networking_auto.js +43 -12
  17. package/lib/engine/engine_networking_auto.js.map +1 -1
  18. package/lib/engine/engine_types.d.ts +4 -2
  19. package/lib/engine/engine_types.js.map +1 -1
  20. package/lib/engine-components/DragControls.d.ts +5 -1
  21. package/lib/engine-components/DragControls.js +32 -3
  22. package/lib/engine-components/DragControls.js.map +1 -1
  23. package/lib/engine-components/timeline/TimelineTracks.js +31 -14
  24. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  25. package/lib/engine-components/ui/Canvas.js +6 -1
  26. package/lib/engine-components/ui/Canvas.js.map +1 -1
  27. package/lib/engine-components/ui/Text.js +2 -1
  28. package/lib/engine-components/ui/Text.js.map +1 -1
  29. package/lib/engine-components/webxr/WebXR.js +6 -0
  30. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/engine/engine_element_overlay.ts +17 -30
  33. package/src/engine/engine_input.ts +9 -4
  34. package/src/engine/engine_mainloop_utils.ts +8 -3
  35. package/src/engine/engine_networking_auto.ts +48 -22
  36. package/src/engine/engine_types.ts +6 -3
  37. package/src/engine-components/DragControls.ts +24 -3
  38. package/src/engine-components/timeline/TimelineTracks.ts +30 -13
  39. package/src/engine-components/ui/Canvas.ts +9 -1
  40. package/src/engine-components/ui/Text.ts +3 -1
  41. package/src/engine-components/webxr/WebXR.ts +6 -0
@@ -5,6 +5,7 @@ import { CubeCamera, Object3D, Scene, WebGLCubeRenderTarget } from 'three';
5
5
  import { IComponent, IContext } from './engine_types';
6
6
  import { isActiveSelf } from './engine_gameobject';
7
7
  import { ContextRegistry } from "./engine_context_registry";
8
+ import { showBalloonWarning, isDevEnvironment, showBalloonMessage, LogType } from "./debug";
8
9
 
9
10
  const debug = getParam("debugnewscripts");
10
11
  const debugHierarchy = getParam("debughierarchy");
@@ -171,8 +172,7 @@ export function processStart(context: IContext, object?: Object3D) {
171
172
  // keep them in queue until script has started
172
173
  // call awake if the script was inactive before
173
174
  utils.safeInvoke(script.__internalAwake.bind(script));
174
- if(script.enabled)
175
- {
175
+ if (script.enabled) {
176
176
  utils.safeInvoke(script.__internalEnable.bind(script));
177
177
  // now call start
178
178
  utils.safeInvoke(script.__internalStart.bind(script));
@@ -232,8 +232,13 @@ export function updateIsActive(obj?: Object3D) {
232
232
  const activeSelf = isActiveSelf(obj);
233
233
  const wasSuccessful = updateIsActiveInHierarchyRecursiveRuntime(obj, activeSelf, true);
234
234
  if (!wasSuccessful) {
235
- console.error("Failed to update active state in hierarchy of \"" + obj.name + "\"", obj);
235
+ if (debug || isDevEnvironment()) {
236
+ console.error("Error updating hierarchy\nDo you have circular references in your project? <a target=\"_blank\" href=\"https://docs.needle.tools/circular-reference\"> Click here for more information.", obj)
237
+ }
238
+ else
239
+ console.error("Failed to update active state in hierarchy of \"" + obj.name + "\"", obj);
236
240
  console.warn(" ↑ this error might be caused by circular references. Please make sure you don't have files with circular references (e.g. one GLB 1 is loading GLB 2 which is then loading GLB 1 again).")
241
+
237
242
  }
238
243
  }
239
244
 
@@ -1,6 +1,6 @@
1
1
  import { getParam } from "./engine_utils";
2
- import { Component } from "../engine-components/Component";
3
- import { RoomEvents } from "./engine_networking";
2
+ import { isDevEnvironment } from "./debug";
3
+ import { IComponent } from "./engine_types";
4
4
 
5
5
  const debug = getParam("debugautosync");
6
6
 
@@ -8,7 +8,7 @@ const $syncerId = Symbol("syncerId");
8
8
  class ComponentsSyncerManager {
9
9
  private _syncers: { [key: string]: ComponentPropertiesSyncer } = {};
10
10
 
11
- getOrCreateSyncer(comp: Component): ComponentPropertiesSyncer | null {
11
+ getOrCreateSyncer(comp: IComponent): ComponentPropertiesSyncer | null {
12
12
  if (!comp.guid) return null;
13
13
  if (this._syncers[comp.guid]) return this._syncers[comp.guid];
14
14
  const syncer = new ComponentPropertiesSyncer(comp);
@@ -28,9 +28,9 @@ const syncerHandler = new ComponentsSyncerManager();
28
28
  */
29
29
  class ComponentPropertiesSyncer {
30
30
 
31
- comp: Component;
31
+ comp: IComponent;
32
32
 
33
- constructor(comp: Component) {
33
+ constructor(comp: IComponent) {
34
34
  // console.log("CREATE NEW SYNC", comp.name, comp.guid);
35
35
  this.comp = comp;
36
36
  }
@@ -115,8 +115,7 @@ class ComponentPropertiesSyncer {
115
115
  if (!this.comp) return;
116
116
  const guid = val.guid;
117
117
  if (guid && guid !== this.comp.guid) return;
118
- if (debug)
119
- console.log("RECEIVED", this.comp.name, this.comp.guid, val);
118
+ if (debug) console.log("RECEIVED", this.comp.name, this.comp.guid, val);
120
119
  try {
121
120
  this._isReceiving = true;
122
121
  for (const key in val) {
@@ -124,6 +123,7 @@ class ComponentPropertiesSyncer {
124
123
  // TODO: maybe use serializable here?!
125
124
  const value = val[key];
126
125
  this.comp[key] = value;
126
+ if(debug) console.log("SET", key, value);
127
127
  }
128
128
  }
129
129
  catch (err) {
@@ -164,13 +164,16 @@ function testValueChanged(newValue, previousValue): boolean {
164
164
  // }
165
165
  }
166
166
  else if (typeof newValue === "object" && typeof previousValue === "object") {
167
- // do we want to traverse / recursively check if anything changed???
168
- for (const key of Object.keys(newValue)) {
169
- if (newValue[key] !== previousValue[key]) {
170
- valueChanged = true;
171
- break;
172
- }
173
- }
167
+ valueChanged = true;
168
+ // The following code doesnt work because the object is a reference type
169
+ // To properly detect changes we would have to detect assignments for each property #
170
+ // OR keep a copy of the previous object
171
+ // for (const key of Object.keys(newValue)) {
172
+ // if (newValue[key] !== previousValue[key]) {
173
+ // valueChanged = true;
174
+ // break;
175
+ // }
176
+ // }
174
177
  }
175
178
  }
176
179
  return valueChanged;
@@ -203,14 +206,17 @@ export declare type SyncFieldOptions = {
203
206
  export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean;
204
207
 
205
208
  /**
206
- * Decorate a field to be automatically networked synced
209
+ * **Decorate a field to be automatically networked synced**
210
+ * *Primitive* values are all automatically synced (like string, boolean, number).
211
+ * For *arrays or objects* make sure to re-assign them (e.g. `this.mySyncField = this.mySyncField`) to trigger an update
212
+ *
207
213
  * @param onFieldChanged name of a callback function that will be called when the field is changed.
208
214
  * You can also pass in a function like so: syncField(myClass.prototype.myFunctionToBeCalled)
209
215
  * This function may return false to prevent notifyChanged from being called
210
216
  * (for example a networked color is sent as a number and may be converted to a color in the receiver again)
211
217
  * Parameters: (newValue, previousValue)
212
218
  */
213
- export const syncField = function(onFieldChanged?: string | FieldChangedCallbackFn) {
219
+ export const syncField = function (onFieldChanged?: string | FieldChangedCallbackFn) {
214
220
 
215
221
  return function (target: any, propertyKey: string) {
216
222
 
@@ -225,6 +231,11 @@ export const syncField = function(onFieldChanged?: string | FieldChangedCallback
225
231
 
226
232
  const t = target;
227
233
  const internalAwake = t.__internalAwake;
234
+ if (typeof internalAwake !== "function") {
235
+ if (debug || isDevEnvironment())
236
+ console.error("@syncField can currently only used on Needle Engine Components, custom object of type \"" + target?.constructor?.name + "\" is not supported", target);
237
+ return;
238
+ }
228
239
  if (debug)
229
240
  console.log(propertyKey);
230
241
  const backingFieldName = Symbol(propertyKey);
@@ -234,19 +245,34 @@ export const syncField = function(onFieldChanged?: string | FieldChangedCallback
234
245
  return;
235
246
  }
236
247
  this[backingFieldName] = this[propertyKey];
237
- internalAwake.call(this);
238
248
 
239
249
  syncer = syncerHandler.getOrCreateSyncer(this);
240
250
 
241
251
  const desc = Object.getOwnPropertyDescriptor(this, propertyKey);
242
252
  if (desc?.set === undefined) {
253
+ let invokingCallback = false;
243
254
  Object.defineProperty(this, propertyKey, {
244
255
  set: function (value) {
245
256
  const oldValue = this[backingFieldName];
246
257
  this[backingFieldName] = value;
247
- if (testValueChanged(value, oldValue)) {
248
- if (fn?.call(this, value, oldValue) !== false)
249
- getSyncer(this)?.notifyChanged(propertyKey, value);
258
+ // Prevent recursive calls when object is assigned in callback
259
+ if (invokingCallback) {
260
+ if (isDevEnvironment())
261
+ console.warn("Recursive call detected", propertyKey);
262
+ return;
263
+ }
264
+ invokingCallback = true;
265
+ try {
266
+ const valueChanged = testValueChanged(value, oldValue);
267
+ if (debug) console.log("SyncField assignment", propertyKey, "changed?", valueChanged, value);
268
+ if (valueChanged) {
269
+ if (fn?.call(this, value, oldValue) !== false) {
270
+ getSyncer(this)?.notifyChanged(propertyKey, value);
271
+ }
272
+ }
273
+ }
274
+ finally {
275
+ invokingCallback = false;
250
276
  }
251
277
  },
252
278
  get: function () {
@@ -258,7 +284,7 @@ export const syncField = function(onFieldChanged?: string | FieldChangedCallback
258
284
  }
259
285
 
260
286
  syncer?.init(this);
261
-
287
+ internalAwake.call(this);
262
288
  }
263
289
 
264
290
  const internalDestroy = t.__internalDestroy;
@@ -281,7 +307,7 @@ export const sync = function (_options?: SyncOptions) {
281
307
 
282
308
  return function <T>(target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
283
309
  // override awake
284
- const comp = target as Component;
310
+ const comp = target as IComponent;
285
311
  let syncer: ComponentPropertiesSyncer | null;
286
312
  const internalAwake = comp.__internalAwake.bind(comp);
287
313
  comp.__internalAwake = function () {
@@ -123,12 +123,15 @@ export declare interface IGameObject extends Object3D {
123
123
  getComponentsInParent<T>(type: Constructor<T>, arr?: T[]): Array<T>;
124
124
  }
125
125
 
126
- export interface IComponent {
127
- get isComponent(): boolean;
126
+ export interface IHasGuid {
127
+ guid: string;
128
+ }
128
129
 
130
+ export interface IComponent extends IHasGuid {
131
+ get isComponent(): boolean;
129
132
 
130
133
  gameObject: IGameObject;
131
- guid: string;
134
+ // guid: string;
132
135
  enabled: boolean;
133
136
  sourceId?: SourceIdentifier;
134
137
 
@@ -13,6 +13,8 @@ import { nameofFactory } from "../engine/engine_utils";
13
13
  import { InstancingUtil } from "../engine/engine_instancing";
14
14
  import { OrbitControls } from "./OrbitControls";
15
15
  import { BufferGeometry, Camera, Color, Line, LineBasicMaterial, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Ray, Raycaster, SphereGeometry, Vector2, Vector3 } from "three";
16
+ import { ObjectRaycaster } from "./ui/Raycaster";
17
+ import { serializable } from "../engine/engine_serialization_decorator";
16
18
 
17
19
  const debug = false;
18
20
 
@@ -37,6 +39,13 @@ export class DragControls extends Interactable implements IPointerDownHandler, I
37
39
  private static _active: number = 0;
38
40
  public static get HasAnySelected(): boolean { return this._active > 0; }
39
41
 
42
+ /** Show's drag gizmos when enabled */
43
+ @serializable()
44
+ public showGizmo: boolean = true;
45
+
46
+ /** When enabled DragControls will drag vertically when the object is viewed from a low angle */
47
+ @serializable()
48
+ public useViewAngle: boolean = true;
40
49
 
41
50
  public transformSelf: boolean = true;
42
51
  // public transformGroup: boolean = true;
@@ -47,6 +56,7 @@ export class DragControls extends Interactable implements IPointerDownHandler, I
47
56
 
48
57
  private selectStartEventListener: ((controls: DragControls, args: SelectArgs) => void)[] = [];
49
58
  private selectEndEventListener: Array<Function> = [];
59
+ private _dragHelper: DragHelper | null = null;
50
60
 
51
61
  constructor() {
52
62
  super();
@@ -66,11 +76,13 @@ export class DragControls extends Interactable implements IPointerDownHandler, I
66
76
  }
67
77
  }
68
78
 
69
- private _dragHelper: DragHelper | null = null;
70
79
 
71
80
 
72
81
  start() {
73
82
  this.orbit = GameObject.findObjectOfType(OrbitControls, this.context);
83
+ if (!this.gameObject.getComponentInParent(ObjectRaycaster)) {
84
+ this.gameObject.addNewComponent(ObjectRaycaster);
85
+ }
74
86
  }
75
87
 
76
88
  private static lastHovered: Object3D;
@@ -234,6 +246,8 @@ export class DragControls extends Interactable implements IPointerDownHandler, I
234
246
 
235
247
  private onUpdateDrag() {
236
248
  if (!this._dragHelper) return;
249
+ this._dragHelper.showGizmo = this.showGizmo;
250
+ this._dragHelper.useViewAngle = this.useViewAngle;
237
251
 
238
252
  this._dragHelper.onUpdate(this.context);
239
253
  for (const rb of this._draggingRigidbodies) {
@@ -279,6 +293,9 @@ export class DragControls extends Interactable implements IPointerDownHandler, I
279
293
 
280
294
  class DragHelper {
281
295
 
296
+ showGizmo: boolean = true;
297
+ useViewAngle: boolean = true;
298
+
282
299
  public get hasSelected(): boolean {
283
300
  return this._selected !== null && this._selected !== undefined;
284
301
  }
@@ -416,7 +433,10 @@ class DragHelper {
416
433
  const lookDot = Math.abs(lookDirection.dot(this._groundOffsetVector));
417
434
 
418
435
  const switchModeKeyPressed = this._context?.input.isKeyPressed(mainKey) || this._context?.input.isKeyPressed(secondaryKey);
419
- const dragOnGroundPlane = !isRotating && lookDot > .2 && !switchModeKeyPressed && this._context!.input.getPointerPressedCount() <= 1;
436
+ let dragOnGroundPlane = !this.useViewAngle || lookDot > .2;
437
+ if (isRotating || switchModeKeyPressed || this._context!.input.getPointerPressedCount() > 1) {
438
+ dragOnGroundPlane = false;
439
+ }
420
440
  const changed = this._didDragOnGroundPlaneLastFrame !== dragOnGroundPlane;
421
441
  this._didDragOnGroundPlaneLastFrame = dragOnGroundPlane;
422
442
 
@@ -477,8 +497,9 @@ class DragHelper {
477
497
  this._groundLine.scale.y = this._groundDistance;
478
498
  }
479
499
  else this._groundLine.scale.y = 1000;
500
+ this._groundLine.visible = this.showGizmo;
480
501
 
481
- this._groundMarker.visible = pointOnPlane !== null;
502
+ this._groundMarker.visible = pointOnPlane !== null && this.showGizmo;
482
503
  if (pointOnPlane) {
483
504
  const s = getWorldPosition(this._camera).distanceTo(pointOnPlane) * .01;
484
505
  this._groundMarker.scale.set(s, s, s);
@@ -331,9 +331,19 @@ export class AnimationTrackHandler extends TrackHandler {
331
331
 
332
332
  let handleLoop = isInTimeRange;
333
333
  if (doPreExtrapolate) {
334
- if (preExtrapolation !== Models.ClipExtrapolation.Hold) {
335
- time += model.start;
336
- handleLoop = true;
334
+ switch (preExtrapolation) {
335
+ case Models.ClipExtrapolation.Hold:
336
+ // Nothing to do
337
+ break;
338
+ case Models.ClipExtrapolation.Loop:
339
+ // TODO: this is not correct yet
340
+ time += model.start;
341
+ handleLoop = true;
342
+ break;
343
+ default:
344
+ time += model.start;
345
+ handleLoop = true;
346
+ break;
337
347
  }
338
348
  }
339
349
 
@@ -341,6 +351,8 @@ export class AnimationTrackHandler extends TrackHandler {
341
351
  let t = this.getClipTime(time, model);
342
352
  let loops = 0;
343
353
  const duration = clipModel.duration;
354
+ // This is the actual duration of the clip in the timeline (with clipping and scale)
355
+ // const clipDuration = (model.end - model.start) * model.timeScale;
344
356
 
345
357
  if (doPreExtrapolate) {
346
358
  if (preExtrapolation === Models.ClipExtrapolation.Hold) {
@@ -358,16 +370,21 @@ export class AnimationTrackHandler extends TrackHandler {
358
370
  }
359
371
  }
360
372
  else if (!isInTimeRange) {
361
- switch (postExtrapolation) {
362
- case Models.ClipExtrapolation.Loop:
363
- t %= duration;
364
- break;
365
- case Models.ClipExtrapolation.PingPong:
366
- const loops = Math.floor(t / duration);
367
- const invert = loops % 2 !== 0;
368
- t %= duration;
369
- if (invert) t = duration - t;
370
- break;
373
+ if (didPostExtrapolate) {
374
+ switch (postExtrapolation) {
375
+ case Models.ClipExtrapolation.Hold:
376
+ t = this.getClipTime(model.end, model);
377
+ break;
378
+ case Models.ClipExtrapolation.Loop:
379
+ t %= duration;
380
+ break;
381
+ case Models.ClipExtrapolation.PingPong:
382
+ const loops = Math.floor(t / duration);
383
+ const invert = loops % 2 !== 0;
384
+ t %= duration;
385
+ if (invert) t = duration - t;
386
+ break;
387
+ }
371
388
  }
372
389
  }
373
390
 
@@ -190,7 +190,15 @@ export class Canvas extends UIRootComponent implements ICanvas {
190
190
  }
191
191
 
192
192
  onBeforeRenderRoutine = () => {
193
- if(this.context.isInVR) return;
193
+
194
+ if (this.context.isInVR) {
195
+ // TODO TMUI @swingingtom - For VR this is so we don't have text clipping
196
+ this.shadowComponent?.updateMatrixWorld(true);
197
+ this.shadowComponent?.updateWorldMatrix(true, true);
198
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
199
+ return;
200
+ }
201
+
194
202
  this.previousParent = this.gameObject.parent;
195
203
  // console.log(this.previousParent?.name + "/" + this.gameObject.name);
196
204
 
@@ -103,7 +103,9 @@ export class Text extends Graphic implements IHasAlphaFactor {
103
103
  }
104
104
 
105
105
  onBeforeRender(): void {
106
- if (this.uiObject && (this.Root as any as ICanvas).screenspace) {
106
+ // TODO TMUI @swingingtom this is so we don't have text clipping
107
+ if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR))
108
+ {
107
109
  this.updateOverflow();
108
110
  }
109
111
  }
@@ -604,6 +604,12 @@ export class WebAR {
604
604
 
605
605
  if (!this.sessionRoot || this.sessionRoot.destroyed || !this.sessionRoot.activeAndEnabled)
606
606
  this.sessionRoot = GameObject.findObjectOfType(WebARSessionRoot, context);
607
+ if (!this.sessionRoot) {
608
+ // TODO: adding it on the scene directly doesnt work (probably because then everything in the scene is disabled including this component). See code a bit furhter below where we add this component to a temporary object inside the scene
609
+ const obj = this.webxr.gameObject;
610
+ this.sessionRoot = GameObject.addNewComponent(obj, WebARSessionRoot);
611
+ console.warn("WebAR: No ARSessionRoot found, creating one automatically on the WebXR object");
612
+ }
607
613
 
608
614
  this.previousBackground = context.scene.background;
609
615
  this.previousEnvironment = context.scene.environment;