Needle Engine

Changes between version 3.2.15-alpha and 3.3.0-alpha
Files changed (23) hide show
  1. src/engine/codegen/register_types.js +4 -2
  2. src/engine-components/AnimatorController.ts +7 -1
  3. src/engine-components/ui/Button.ts +14 -9
  4. src/engine-components/ui/Canvas.ts +104 -40
  5. src/engine-components/codegen/components.ts +2 -1
  6. src/engine/engine_addressables.ts +28 -10
  7. src/engine/engine_element.ts +1 -1
  8. src/engine/engine_gameobject.ts +18 -1
  9. src/engine/engine_input.ts +11 -0
  10. src/engine/engine_math.ts +10 -0
  11. src/engine-components/ui/EventSystem.ts +16 -9
  12. src/engine-components/ui/Graphic.ts +44 -8
  13. src/engine-components/ui/Image.ts +10 -1
  14. src/engine-components/ui/InputField.ts +9 -1
  15. src/engine-components/ui/Interfaces.ts +12 -0
  16. src/engine-components/ui/Keyboard.ts +0 -204
  17. src/engine-components/OrbitControls.ts +14 -6
  18. src/engine-components/ui/RectTransform.ts +201 -58
  19. src/engine-components/ui/Text.ts +281 -262
  20. src/engine-components/TransformGizmo.ts +63 -69
  21. src/engine-components/webxr/WebXRImageTracking.ts +85 -10
  22. src/engine-components/utils/LookAt.ts +21 -0
  23. src/engine-components/ui/Outline.ts +13 -0
src/engine/codegen/register_types.js CHANGED
@@ -76,13 +76,13 @@
76
76
  import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules";
77
77
  import { InputField } from "../../engine-components/ui/InputField";
78
78
  import { Interactable } from "../../engine-components/Interactable";
79
- import { Keyboard } from "../../engine-components/ui/Keyboard";
80
79
  import { LayoutGroup } from "../../engine-components/ui/Layout";
81
80
  import { Light } from "../../engine-components/Light";
82
81
  import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules";
83
82
  import { LODGroup } from "../../engine-components/LODGroup";
84
83
  import { LODModel } from "../../engine-components/LODGroup";
85
84
  import { LogStats } from "../../engine-components/debug/LogStats";
85
+ import { LookAt } from "../../engine-components/utils/LookAt";
86
86
  import { LookAtConstraint } from "../../engine-components/LookAtConstraint";
87
87
  import { MainModule } from "../../engine-components/ParticleSystemModules";
88
88
  import { MaskableGraphic } from "../../engine-components/ui/Graphic";
@@ -97,6 +97,7 @@
97
97
  import { OffsetConstraint } from "../../engine-components/OffsetConstraint";
98
98
  import { OpenURL } from "../../engine-components/utils/OpenURL";
99
99
  import { OrbitControls } from "../../engine-components/OrbitControls";
100
+ import { Outline } from "../../engine-components/ui/Outline";
100
101
  import { ParticleBurst } from "../../engine-components/ParticleSystemModules";
101
102
  import { ParticleSubEmitter } from "../../engine-components/ParticleSystemSubEmitter";
102
103
  import { ParticleSystem } from "../../engine-components/ParticleSystem";
@@ -266,13 +267,13 @@
266
267
  TypeStore.add("InheritVelocityModule", InheritVelocityModule);
267
268
  TypeStore.add("InputField", InputField);
268
269
  TypeStore.add("Interactable", Interactable);
269
- TypeStore.add("Keyboard", Keyboard);
270
270
  TypeStore.add("LayoutGroup", LayoutGroup);
271
271
  TypeStore.add("Light", Light);
272
272
  TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
273
273
  TypeStore.add("LODGroup", LODGroup);
274
274
  TypeStore.add("LODModel", LODModel);
275
275
  TypeStore.add("LogStats", LogStats);
276
+ TypeStore.add("LookAt", LookAt);
276
277
  TypeStore.add("LookAtConstraint", LookAtConstraint);
277
278
  TypeStore.add("MainModule", MainModule);
278
279
  TypeStore.add("MaskableGraphic", MaskableGraphic);
@@ -287,6 +288,7 @@
287
288
  TypeStore.add("OffsetConstraint", OffsetConstraint);
288
289
  TypeStore.add("OpenURL", OpenURL);
289
290
  TypeStore.add("OrbitControls", OrbitControls);
291
+ TypeStore.add("Outline", Outline);
290
292
  TypeStore.add("ParticleBurst", ParticleBurst);
291
293
  TypeStore.add("ParticleSubEmitter", ParticleSubEmitter);
292
294
  TypeStore.add("ParticleSystem", ParticleSystem);
src/engine-components/AnimatorController.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { assign } from "../engine/engine_serialization_core";
8
8
  import { Mathf } from "../engine/engine_math";
9
9
  import { isAnimationAction } from "../engine/engine_three_utils";
10
+ import { isDevEnvironment } from "../engine/debug";
10
11
 
11
12
  const debug = getParam("debuganimatorcontroller");
12
13
  const debugRootMotion = getParam("debugrootmotion");
@@ -407,7 +408,12 @@
407
408
  }
408
409
  }
409
410
  }
410
- else console.warn("No action", state.motion, this);
411
+ else if (isDevEnvironment()) {
412
+ if (!state["__warned_no_motion"]) {
413
+ state["__warned_no_motion"] = true;
414
+ console.warn("No action", state.motion, this);
415
+ }
416
+ }
411
417
 
412
418
  if (debug)
413
419
  console.log("TRANSITION FROM " + prev?.name + " TO " + state.name, durationInSec, prevAction, action, action?.getEffectiveTimeScale(), action?.getEffectiveWeight(), action?.isRunning(), action?.isScheduled(), action?.paused);
src/engine-components/ui/Button.ts CHANGED
@@ -69,9 +69,9 @@
69
69
  console.log("Button Enter", this.animationTriggers?.highlightedTrigger, this.animator);
70
70
  this._isHovered = true;
71
71
  if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
72
- this.animator.SetTrigger(this.animationTriggers.highlightedTrigger);
72
+ this.animator.setTrigger(this.animationTriggers.highlightedTrigger);
73
73
  }
74
- else if(this.transition === Transition.ColorTint && this.colors) {
74
+ else if (this.transition === Transition.ColorTint && this.colors) {
75
75
  this._image?.setState("hovered");
76
76
  }
77
77
  this.context.input.setCursorPointer();
@@ -82,9 +82,9 @@
82
82
  console.log("Button Exit", this.animationTriggers?.highlightedTrigger, this.animator);
83
83
  this._isHovered = false;
84
84
  if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
85
- this.animator.SetTrigger(this.animationTriggers.normalTrigger);
85
+ this.animator.setTrigger(this.animationTriggers.normalTrigger);
86
86
  }
87
- else if(this.transition === Transition.ColorTint && this.colors) {
87
+ else if (this.transition === Transition.ColorTint && this.colors) {
88
88
  this._image?.setState("normal");
89
89
  }
90
90
  this.context.input.setCursorNormal();
@@ -94,9 +94,9 @@
94
94
  if (debug)
95
95
  console.log("Button Down", this.animationTriggers?.highlightedTrigger, this.animator);
96
96
  if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
97
- this.animator.SetTrigger(this.animationTriggers.pressedTrigger);
97
+ this.animator.setTrigger(this.animationTriggers.pressedTrigger);
98
98
  }
99
- else if(this.transition === Transition.ColorTint && this.colors) {
99
+ else if (this.transition === Transition.ColorTint && this.colors) {
100
100
  this._image?.setState("pressed");
101
101
  }
102
102
  }
@@ -105,9 +105,9 @@
105
105
  if (debug)
106
106
  console.warn("Button Up", this.animationTriggers?.highlightedTrigger, this.animator, this._isHovered);
107
107
  if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
108
- this.animator.SetTrigger(this._isHovered ? this.animationTriggers.highlightedTrigger : this.animationTriggers.normalTrigger);
108
+ this.animator.setTrigger(this._isHovered ? this.animationTriggers.highlightedTrigger : this.animationTriggers.normalTrigger);
109
109
  }
110
- else if(this.transition === Transition.ColorTint && this.colors) {
110
+ else if (this.transition === Transition.ColorTint && this.colors) {
111
111
  this._image?.setState(this._isHovered ? "hovered" : "normal");
112
112
  }
113
113
  }
@@ -197,6 +197,10 @@
197
197
  private stateSetup(image: Image) {
198
198
  image.setInteractable(this.interactable);
199
199
 
200
+ // @marwie : If this piece of code could be moved to the SimpleStateBehavior instanciation location,
201
+ // Its setup could be eased :
202
+ // @see https://github.com/felixmariotto/three-mesh-ui/blob/7.1.x/examples/ex__keyboard.js#L407
203
+
200
204
  const normal = this.getFinalColor(image.color, this.colors?.normalColor);
201
205
  const normalState = {
202
206
  state: "normal",
@@ -242,7 +246,8 @@
242
246
  state: "disabled",
243
247
  attributes: {
244
248
  backgroundColor: disabled,
245
- backgroundOpacity: disabled.alpha,
249
+ // @marwie, this disabled alpha property doesn't seem to have the opacity requested in unity
250
+ backgroundOpacity: disabled.alpha
246
251
  }
247
252
  };
248
253
  image.setupState(disabledState);
src/engine-components/ui/Canvas.ts CHANGED
@@ -2,13 +2,13 @@
2
2
  import { serializable } from "../../engine/engine_serialization_decorator";
3
3
  import { FrameEvent } from "../../engine/engine_setup";
4
4
  import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent";
5
- import { Mathf } from "../../engine/engine_math";
6
- import * as THREE from "three";
7
- import { getComponentsInChildren } from "../../engine/engine_components";
8
- import { IComponent } from "../../engine/engine_types";
9
5
  import { GameObject } from "../Component";
10
- import { showBalloonMessage, showBalloonWarning } from "../../engine/debug";
11
6
  import { Object3D } from "three";
7
+ import { RectTransform } from "./RectTransform";
8
+ import { ICanvas } from "./Interfaces";
9
+ import { Camera } from "../Camera";
10
+ import { EventSystem } from "./EventSystem";
11
+ import * as ThreeMeshUI from 'three-mesh-ui'
12
12
 
13
13
  export enum RenderMode {
14
14
  ScreenSpaceOverlay = 0,
@@ -17,8 +17,12 @@
17
17
  Undefined = -1,
18
18
  }
19
19
 
20
- export class Canvas extends UIRootComponent {
20
+ export class Canvas extends UIRootComponent implements ICanvas {
21
21
 
22
+ get screenspace(): any {
23
+ return this.renderMode !== RenderMode.WorldSpace;
24
+ }
25
+
22
26
  @serializable()
23
27
  set renderOnTop(val: boolean) {
24
28
  if (val === this._renderOnTop) {
@@ -27,8 +31,15 @@
27
31
  this._renderOnTop = val;
28
32
  this.onRenderSettingsChanged();
29
33
  }
30
- get renderOnTop() { return this._renderOnTop; }
31
- private _renderOnTop: boolean = false;
34
+ get renderOnTop() {
35
+ if (this._renderOnTop !== undefined) return this._renderOnTop;
36
+ if (this.screenspace) {
37
+ // Render ScreenSpaceOverlay always on top
38
+ if (this._renderMode === RenderMode.ScreenSpaceOverlay) return true;
39
+ }
40
+ return false;
41
+ }
42
+ private _renderOnTop: boolean | undefined;
32
43
 
33
44
  @serializable()
34
45
  set depthWrite(val: boolean) {
@@ -99,7 +110,14 @@
99
110
  this._scaleFactor = val;
100
111
  }
101
112
 
113
+ @serializable(Camera)
114
+ worldCamera?: Camera;
115
+
116
+ @serializable()
117
+ planeDistance: number = -1;
118
+
102
119
  awake() {
120
+ //@ts-ignore
103
121
  this.shadowComponent = this.gameObject;
104
122
  super.awake();
105
123
  }
@@ -108,34 +126,59 @@
108
126
  super.onEnable();
109
127
  this._updateRenderSettingsRoutine = undefined;
110
128
  this.onRenderSettingsChanged();
129
+ document.addEventListener("resize", this._boundRenderSettingsChanged);
130
+ // We want to run AFTER all regular onBeforeRender callbacks
131
+ this.context.pre_render_callbacks.push(this.onBeforeRenderRoutine);
132
+ this.context.post_render_callbacks.push(this.onAfterRenderRoutine);
111
133
  }
112
134
 
113
- private previousAspect: number = -1;
135
+ onDisable(): void {
136
+ super.onDisable();
137
+ document.removeEventListener("resize", this._boundRenderSettingsChanged);
138
+ // Remove callbacks
139
+ const preRenderIndex = this.context.pre_render_callbacks.indexOf(this.onBeforeRenderRoutine);
140
+ if (preRenderIndex !== -1) {
141
+ this.context.pre_render_callbacks.splice(preRenderIndex, 1);
142
+ }
143
+ const postRenderIndex = this.context.post_render_callbacks.indexOf(this.onAfterRenderRoutine);
144
+ if (postRenderIndex !== -1) {
145
+ this.context.post_render_callbacks.splice(postRenderIndex, 1);
146
+ }
147
+ }
148
+
149
+ private _boundRenderSettingsChanged = this.onRenderSettingsChanged.bind(this);
150
+
114
151
  private previousParent: Object3D | null = null;
115
152
 
116
- onBeforeRender() {
117
- if (this.isScreenSpace && this.context.mainCameraComponent && this.context.mainCameraComponent.aspect !== this.previousAspect) {
118
- this.previousAspect = this.context.mainCameraComponent.aspect;
119
- this.updateRenderMode();
120
- }
121
- else if(this.renderOnTop){
153
+ onBeforeRenderRoutine = () => {
154
+ if (this.renderOnTop) {
122
155
  // This is just a test but in reality it should be combined with all world canvases with render on top in one render pass
123
156
  this.previousParent = this.gameObject.parent;
124
157
  this.gameObject.removeFromParent();
125
158
  }
159
+ else {
160
+ this.onUpdateRenderMode();
161
+ // TODO: we might need to optimize this. This is here to make sure the TMUI text clipping matrices are correct. Ideally the text does use onBeforeRender and apply the clipping matrix there so we dont have to force update all the matrices here
162
+ this.shadowComponent?.updateMatrixWorld(true);
163
+ this.shadowComponent?.updateWorldMatrix(true, true);
164
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
165
+ }
126
166
  }
127
167
 
128
- onAfterRender(): void {
168
+ onAfterRenderRoutine = () => {
129
169
  if (this.renderOnTop && this.previousParent && this.context.mainCamera) {
130
170
  this.previousParent.add(this.gameObject);
131
171
  this.context.renderer.autoClear = false;
132
172
  this.context.renderer.clearDepth();
173
+ this.onUpdateRenderMode(true);
174
+ this.shadowComponent?.updateMatrixWorld(true);
175
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
133
176
  this.context.renderer.render(this.gameObject, this.context.mainCamera);
134
177
  this.context.renderer.autoClear = true;
135
178
  }
136
179
  }
137
180
 
138
- applyRenderSettings(){
181
+ applyRenderSettings() {
139
182
  this.onRenderSettingsChanged();
140
183
  }
141
184
 
@@ -149,7 +192,7 @@
149
192
  yield;
150
193
  this._updateRenderSettingsRoutine = undefined;
151
194
  if (this.shadowComponent) {
152
- this.updateRenderMode();
195
+ this.onUpdateRenderMode();
153
196
  // this.onWillUpdateRenderSettings();
154
197
  updateRenderSettingsRecursive(this.shadowComponent, this);
155
198
  for (const ch of GameObject.getComponentsInChildren(this.gameObject, BaseUIComponent)) {
@@ -159,44 +202,65 @@
159
202
  }
160
203
 
161
204
  private _activeRenderMode: RenderMode = -1;
205
+ private _lastWidth: number = -1;
206
+ private _lastHeight: number = -1;
162
207
 
163
- private get isScreenSpace(): boolean {
164
- return this._activeRenderMode === RenderMode.ScreenSpaceCamera || this._activeRenderMode === RenderMode.ScreenSpaceOverlay;
165
- }
208
+ private onUpdateRenderMode(force: boolean = false) {
209
+ if (!force) {
210
+ if (this._renderMode === this._activeRenderMode && this._lastWidth === this.context.domWidth && this._lastHeight === this.context.domHeight) {
211
+ return;
212
+ }
213
+ }
214
+ this._activeRenderMode = this._renderMode;
215
+ let camera = this.context.mainCameraComponent;
216
+ let planeDistance: number = camera?.farClipPlane ?? 100;
217
+ if (this._renderMode === RenderMode.ScreenSpaceCamera) {
218
+ if (this.worldCamera)
219
+ camera = this.worldCamera as Camera;
220
+ if (this.planeDistance > 0)
221
+ planeDistance = this.planeDistance;
222
+ }
166
223
 
167
- private updateRenderMode() {
168
- if (this.renderMode === this._activeRenderMode) return;
169
- switch (this.renderMode) {
224
+ switch (this._renderMode) {
170
225
  case RenderMode.ScreenSpaceOverlay:
171
226
  case RenderMode.ScreenSpaceCamera:
172
- showBalloonWarning("Screenspace Canvas is not supported yet. Please use worldspace");
173
- const camera = this.context.mainCameraComponent;
227
+ this._lastWidth = this.context.domWidth;
228
+ this._lastHeight = this.context.domHeight;
229
+
230
+ // showBalloonWarning("Screenspace Canvas is not supported yet. Please use worldspace");
174
231
  if (!camera) return;
232
+
175
233
  const canvas = this.gameObject;
176
234
  const camObj = camera.gameObject;
177
235
  camObj?.add(canvas);
178
- const pos = camera.farClipPlane;
236
+ // we move the plane SLIGHTLY closer to be sure not to cull the canvas
237
+ const plane = planeDistance - .1;
179
238
  canvas.position.x = 0;
180
239
  canvas.position.y = 0;
181
- canvas.position.z = -pos;
240
+ canvas.position.z = -plane;
241
+ canvas.quaternion.identity();
182
242
 
183
- // console.log(this.shadowComponent)
243
+ const rect = this.gameObject.getComponent(RectTransform)!;
184
244
 
185
- if (camera.fieldOfView) {
186
- const w = Math.tan(Mathf.toRadians(camera.fieldOfView) * pos) * (camera.aspect * 1.333333);
187
- const h = w * (this.context.domHeight / this.context.domWidth);
188
- canvas.scale.x = -w;
189
- canvas.scale.y = h;
245
+ const vFOV = camera.fieldOfView! * Math.PI / 180;
246
+ const h = 2 * Math.tan(vFOV / 2) * Math.abs(plane);
247
+ canvas.scale.x = h / this.context.domHeight;
248
+ canvas.scale.y = h / this.context.domHeight;
249
+ // Set scale.z, otherwise small offsets in screenspace mode have different visual results based on export scale and other settings
250
+ canvas.scale.z = .01;
251
+ rect.sizeDelta.x = this.context.domWidth;
252
+ rect.sizeDelta.y = this.context.domHeight;
253
+ rect?.markDirty();
190
254
 
191
- }
192
- // const rects = this.gameObject.getComponentsInChildren(BaseUIComponent);
193
- // for (const rect of rects) {
194
- // rect.set({ width: this.context.domWidth * .5, height: 100 })
195
- // }
196
255
 
256
+ // this.context.scene.add(this.gameObject)
257
+ // this.gameObject.scale.multiplyScalar(.01);
258
+ // this.gameObject.position.set(0,0,0);
259
+
197
260
  break;
198
261
  case RenderMode.WorldSpace:
199
-
262
+ this._lastWidth = -1;
263
+ this._lastHeight = -1;
200
264
  break;
201
265
  }
202
266
  }
src/engine-components/codegen/components.ts CHANGED
@@ -74,13 +74,13 @@
74
74
  export { InheritVelocityModule } from "../ParticleSystemModules";
75
75
  export { InputField } from "../ui/InputField";
76
76
  export { Interactable } from "../Interactable";
77
- export { Keyboard } from "../ui/Keyboard";
78
77
  export { LayoutGroup } from "../ui/Layout";
79
78
  export { Light } from "../Light";
80
79
  export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules";
81
80
  export { LODGroup } from "../LODGroup";
82
81
  export { LODModel } from "../LODGroup";
83
82
  export { LogStats } from "../debug/LogStats";
83
+ export { LookAt } from "../utils/LookAt";
84
84
  export { LookAtConstraint } from "../LookAtConstraint";
85
85
  export { MainModule } from "../ParticleSystemModules";
86
86
  export { MaskableGraphic } from "../ui/Graphic";
@@ -95,6 +95,7 @@
95
95
  export { OffsetConstraint } from "../OffsetConstraint";
96
96
  export { OpenURL } from "../utils/OpenURL";
97
97
  export { OrbitControls } from "../OrbitControls";
98
+ export { Outline } from "../ui/Outline";
98
99
  export { ParticleBurst } from "../ParticleSystemModules";
99
100
  export { ParticleSubEmitter } from "../ParticleSystemSubEmitter";
100
101
  export { ParticleSystem } from "../ParticleSystem";
src/engine/engine_addressables.ts CHANGED
@@ -27,8 +27,8 @@
27
27
 
28
28
  private _assetReferences: { [key: string]: AssetReference } = {};
29
29
 
30
- findAssetReference(uri: string): AssetReference | null {
31
- return this._assetReferences[uri] || null;
30
+ findAssetReference(key: string): AssetReference | null {
31
+ return this._assetReferences[key] || null;
32
32
  }
33
33
 
34
34
  registerAssetReference(ref: AssetReference): AssetReference {
@@ -48,9 +48,9 @@
48
48
 
49
49
  export class AssetReference {
50
50
 
51
- static getOrCreate(sourceId: SourceIdentifier, uri: string, context: Context): AssetReference {
52
- const fullPath = resolveUrl(sourceId, uri);
53
- if (debug) console.log("GetOrCreate Addressable from", sourceId, uri, "FinalPath=", fullPath);
51
+ static getOrCreate(sourceId: SourceIdentifier, url: string, context: Context): AssetReference {
52
+ const fullPath = resolveUrl(sourceId, url);
53
+ if (debug) console.log("GetOrCreate Addressable from", sourceId, url, "FinalPath=", fullPath);
54
54
  const addressables = context.addressables;
55
55
  const existing = addressables.findAssetReference(fullPath);
56
56
  if (existing) return existing;
@@ -89,19 +89,19 @@
89
89
  private _isLoadingRawBinary: boolean = false;
90
90
  private _rawBinary?: ArrayBuffer | null;
91
91
 
92
- constructor(uri: string, hash?: string) {
92
+ constructor(uri: string, hash?: string, asset: any = null) {
93
93
  this._url = uri;
94
94
  this._hash = hash;
95
95
  if (uri.includes("?v="))
96
96
  this._hashedUri = uri;
97
97
  else
98
98
  this._hashedUri = hash ? uri + "?v=" + hash : uri;
99
-
99
+ if (asset !== null) this.asset = asset;
100
100
  registerPrefabProvider(this._url, this.onResolvePrefab.bind(this));
101
101
  }
102
102
 
103
- private async onResolvePrefab(uri: string): Promise<IGameObject | null> {
104
- if (uri === this.uri) {
103
+ private async onResolvePrefab(url: string): Promise<IGameObject | null> {
104
+ if (url === this.uri) {
105
105
  if (this.mustLoad) await this.loadAssetAsync();
106
106
  if (this.asset) {
107
107
  return this.asset;
@@ -320,12 +320,30 @@
320
320
  return null;
321
321
  }
322
322
  if (!context.gltfId) {
323
- console.error("Missing spurce id");
323
+ console.error("Missing source id");
324
324
  return null;
325
325
  }
326
326
  const ref = AssetReference.getOrCreate(context.gltfId, data, context.context);
327
327
  return ref;
328
328
  }
329
+ else if (data instanceof Object3D) {
330
+ if (!context.context) {
331
+ console.error("Missing context");
332
+ return null;
333
+ }
334
+ if (!context.gltfId) {
335
+ console.error("Missing source id");
336
+ return null;
337
+ }
338
+ const obj = data;
339
+ const ctx = context.context;
340
+ const guid = obj["guid"] ?? obj.uuid;
341
+ const existing = ctx.addressables.findAssetReference(guid);
342
+ if (existing) return existing;
343
+ const ref = new AssetReference(guid, undefined, obj);
344
+ ctx.addressables.registerAssetReference(ref);
345
+ return ref;
346
+ }
329
347
  return null;
330
348
  }
331
349
 
src/engine/engine_element.ts CHANGED
@@ -448,5 +448,5 @@
448
448
  let index = name.indexOf("?")
449
449
  if (index > 0)
450
450
  name = name.substring(0, index);
451
- return name;
451
+ return decodeURIComponent(name);
452
452
  }
src/engine/engine_gameobject.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { InstantiateIdProvider } from "./engine_networking_instantiate";
4
4
  import { Context, registerComponent } from "./engine_setup";
5
5
  import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils";
6
- import { GuidsMap, IComponent as Component, IComponent, IGameObject as GameObject, UIDProvider } from "./engine_types";
6
+ import { GuidsMap, IComponent as Component, IComponent, IGameObject as GameObject, UIDProvider, Constructor } from "./engine_types";
7
7
  import { getParam, tryFindObject } from "./engine_utils";
8
8
  import { apply } from "../engine-components/js-extensions/Object3D";
9
9
  import { $isUsingInstancing, InstancingUtil } from "./engine_instancing";
@@ -163,6 +163,23 @@
163
163
  return internalForEachComponent(instance, cb, recursive);
164
164
  }
165
165
 
166
+ export function* foreachComponentEnumerator<T extends IComponent>(instance: Object3D, type?: Constructor<T>, includeChildren: boolean = false): Generator<T> {
167
+ if (!instance?.userData.components) return;
168
+ for (const comp of instance.userData.components) {
169
+ if (type && comp?.isComponent === true && comp instanceof type) {
170
+ yield comp;
171
+ }
172
+ else {
173
+ yield comp;
174
+ }
175
+ }
176
+ if (includeChildren === true) {
177
+ for (const ch of instance.children) {
178
+ yield* foreachComponentEnumerator(ch, type, true);
179
+ }
180
+ }
181
+ }
182
+
166
183
  function internalForEachComponent(instance: Object3D, cb: ForEachComponentCallback, recursive: boolean, level: number = 0): any {
167
184
  if (!instance) return;
168
185
  if (!instance.isObject3D) {
src/engine/engine_input.ts CHANGED
@@ -512,6 +512,17 @@
512
512
  private onDown(evt: PointerEventArgs) {
513
513
  if (debug) console.log(evt.pointerType, "DOWN", evt.button);
514
514
  if (!this.isInRect(evt)) return;
515
+
516
+ // check if we received an mouse UP event for a touch (for some reason we get a mouse.down for touch.up)
517
+ if (evt.pointerType === PointerType.Mouse) {
518
+ const upTime = this._pointerUpTimestamp[evt.button];
519
+ if (upTime > 0 && upTime === evt.source?.timeStamp) {
520
+ // we received an UP event for a touch, ignore this DOWN event
521
+ if(debug) console.log("Ignoring mouse.down for touch.up");
522
+ return;
523
+ }
524
+ }
525
+
515
526
  this.setPointerState(evt.button, this._pointerPressed, true);
516
527
  this.setPointerState(evt.button, this._pointerDown, true);
517
528
  this.setPointerStateT(evt.button, this._pointerEvent, evt.source);
src/engine/engine_math.ts CHANGED
@@ -49,10 +49,20 @@
49
49
  return radians * 180 / Math.PI;
50
50
  }
51
51
 
52
+ readonly Rad2Deg = 180 / Math.PI;
53
+
52
54
  toRadians(degrees: number) {
53
55
  return degrees * Math.PI / 180;
54
56
  }
55
57
 
58
+ readonly Deg2Rad = Math.PI / 180;
59
+
60
+ readonly Epsilon = 0.00001;
61
+
62
+ tan(radians: number) {
63
+ return Math.tan(radians);
64
+ }
65
+
56
66
  gammaToLinear(gamma: number) {
57
67
  return Math.pow(gamma, 2.2);
58
68
  }
src/engine-components/ui/EventSystem.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { OrbitControls } from "../OrbitControls";
8
8
  import { IPointerEventHandler, PointerEventData } from "./PointerEvents";
9
9
  import { ObjectRaycaster, Raycaster } from "./Raycaster";
10
- import { InputEvents } from "../../engine/engine_input";
10
+ import { InputEvents, PointerEventArgs } from "../../engine/engine_input";
11
11
  import { Object3D } from "three";
12
12
  import { ICanvasGroup, IGraphic } from "./Interfaces";
13
13
  import { getParam } from "../../engine/engine_utils";
@@ -22,6 +22,12 @@
22
22
  AfterHandleInput = "AfterHandleInput",
23
23
  }
24
24
 
25
+ export declare type AfterHandleInputEvent = {
26
+ sender: EventSystem,
27
+ args: PointerEventData,
28
+ hasActiveUI: boolean
29
+ }
30
+
25
31
  export class EventSystem extends Behaviour {
26
32
 
27
33
 
@@ -249,16 +255,16 @@
249
255
  if (!hits) return;
250
256
  this.lastPointerEvent = args;
251
257
 
252
- const evt = {
258
+ const evt : AfterHandleInputEvent = {
253
259
  sender: this,
254
260
  args: args,
255
261
  hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
256
262
  }
257
- if(debug && args.isClicked)
258
- showBalloonMessage("EventSystem: " + args.pointerId + " - " + this.context.time.frame + " - Up:" + args.isUp + ", Down:" + args.isDown)
263
+ if (debug && args.isClicked)
264
+ showBalloonMessage("EventSystem: " + args.pointerId + " - " + this.context.time.frame + " - Up:" + args.isUp + ", Down:" + args.isDown)
259
265
  this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }))
260
266
  this.handleIntersections(hits, args);
261
- this.dispatchEvent(new CustomEvent(EventSystemEvents.AfterHandleInput, { detail: evt }))
267
+ this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }))
262
268
  }
263
269
 
264
270
  private _tempComponentsArray: Behaviour[] = [];
@@ -518,8 +524,8 @@
518
524
  if (currentFrame === lu.frame) return;
519
525
  lu.frame = currentFrame;
520
526
  let shouldUpdate = this.needsUpdate || currentFrame < 1;
521
- if(lu.nextUpdate === context.time.frameCount) shouldUpdate = true;
522
- if(this.needsUpdate) lu.nextUpdate = currentFrame + 3;
527
+ if (lu.nextUpdate === context.time.frameCount) shouldUpdate = true;
528
+ // if(this.needsUpdate) lu.nextUpdate = currentFrame + 3;
523
529
  if (shouldUpdate) {
524
530
  if (debug)
525
531
  console.log("Update threemeshui");
@@ -564,8 +570,9 @@
564
570
  static findBlockInParent(elem: any): ThreeMeshUI.Block | null {
565
571
  if (!elem) return null;
566
572
  if (elem.isBlock) {
567
- if (Object.keys(elem.states).length > 0)
568
- return elem;
573
+ // @TODO : Replace states managements
574
+ // if (Object.keys(elem.states).length > 0)
575
+ return elem;
569
576
  }
570
577
  return this.findBlockInParent(elem.parent);
571
578
  }
src/engine-components/ui/Graphic.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IGraphic } from './Interfaces';
1
+ import { IGraphic, IRectTransformChangedReceiver } from './Interfaces';
2
2
  import * as ThreeMeshUI from 'three-mesh-ui'
3
3
  import { RGBAColor } from "../js-extensions/RGBAColor"
4
4
  import { BaseUIComponent } from "./BaseUIComponent";
@@ -7,9 +7,15 @@
7
7
  import { RectTransform } from './RectTransform';
8
8
  import { onChange, scheduleAction } from "./Utils"
9
9
  import { GameObject } from '../Component';
10
+ import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior"
11
+ import { Outline } from './Outline';
10
12
 
13
+ const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number } = {
14
+ backgroundColor: new Color(1, 1, 1),
15
+ backgroundOpacity: 1,
16
+ };
11
17
 
12
- export class Graphic extends BaseUIComponent implements IGraphic {
18
+ export class Graphic extends BaseUIComponent implements IGraphic, IRectTransformChangedReceiver {
13
19
 
14
20
  get isGraphic() { return true; }
15
21
 
@@ -26,9 +32,11 @@
26
32
  }
27
33
  this._color.copy(col);
28
34
  }
35
+
29
36
  protected onColorChanged() {
30
- const newcolor = this.color;
31
- this.setOptions({ backgroundColor: newcolor, backgroundOpacity: newcolor.alpha, borderOpacity: newcolor.alpha });
37
+ _colorStateObject.backgroundColor = this._color;
38
+ _colorStateObject.backgroundOpacity = this._color.alpha;
39
+ this.uiObject?.set(_colorStateObject);
32
40
  }
33
41
 
34
42
  // used via animations
@@ -44,6 +52,9 @@
44
52
 
45
53
 
46
54
  private _rect: RectTransform | null = null;
55
+
56
+ private _stateManager : SimpleStateBehavior | null = null;
57
+
47
58
  protected get rectTransform(): RectTransform {
48
59
  if (!this._rect) {
49
60
  this._rect = GameObject.getComponent(this.gameObject, RectTransform);
@@ -51,6 +62,11 @@
51
62
  return this._rect!;
52
63
  }
53
64
 
65
+ onParentRectTransformChanged() {
66
+ this.uiObject?.set({ width: this.rectTransform.width, height:this.rectTransform.height })
67
+ this.markDirty();
68
+ }
69
+
54
70
  __internalNewInstanceCreated(): void {
55
71
  super.__internalNewInstanceCreated();
56
72
  this._rect = null;
@@ -63,14 +79,21 @@
63
79
  if (this.uiObject) {
64
80
  //@ts-ignore
65
81
  this.uiObject.setState(state);
82
+ this?.markDirty();
66
83
  }
67
84
  }
68
85
 
69
86
  setupState(state: object) {
70
87
  this.makePanel();
71
88
  if (this.uiObject) {
89
+
90
+ // @marwie : v7.x now have a concurrent state management in core mimicking html/css
91
+ // ie : (::firstChild::hover::disabled) where firstchild, hover and disabled are all on different channels
92
+ // In order to keep needle Raycaster and EventSystem intact, I added in v7 a SimpleStateBehavior, which acts as previously
93
+
94
+ if( !this._stateManager ) this._stateManager = new SimpleStateBehavior(this.uiObject);
72
95
  //@ts-ignore
73
- this.uiObject.setupState(state);
96
+ this.uiObject.setupState(state.state, state.attributes);
74
97
  }
75
98
  }
76
99
 
@@ -79,8 +102,8 @@
79
102
  if (this.uiObject) {
80
103
  //@ts-ignore
81
104
  this.uiObject.set(opts);
82
- if (opts["backgroundColor"] !== undefined || opts["backgroundOpacity"] !== undefined)
83
- this.uiObject["updateBackgroundMaterial"]?.call(this.uiObject);
105
+ // if (opts["backgroundColor"] !== undefined || opts["backgroundOpacity"] !== undefined)
106
+ // this.uiObject["updateBackgroundMaterial"]?.call(this.uiObject);
84
107
  }
85
108
  }
86
109
 
@@ -119,6 +142,7 @@
119
142
  offset: 1, // without a tiny offset we get z fighting
120
143
  };
121
144
  this.onBeforeCreate(opts);
145
+ this.applyEffects(opts);
122
146
  this.onCreate(opts);
123
147
  this.controlsChildLayout = false;
124
148
  this._currentlyCreatingPanel = false;
@@ -133,6 +157,17 @@
133
157
  }
134
158
  protected onAfterCreated() { }
135
159
 
160
+ private applyEffects(opts){
161
+ const outline = this.gameObject?.getComponent(Outline);
162
+ if (outline) {
163
+ if (outline.effectDistance) opts.borderWidth = Math.max(Math.abs(outline.effectDistance.x), Math.abs(outline.effectDistance.y));
164
+ if (outline.effectColor) {
165
+ opts.borderColor = outline.effectColor;
166
+ opts.borderOpacity = outline.effectColor.alpha;
167
+ }
168
+ }
169
+ }
170
+
136
171
  /** used internally to ensure textures assigned to UI use linear encoding */
137
172
  static textureCache: Map<Texture, Texture> = new Map();
138
173
 
@@ -151,13 +186,14 @@
151
186
  tex = clone;
152
187
  }
153
188
  }
154
- this.setOptions({ backgroundTexture: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
189
+ this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
155
190
  }
156
191
  }
157
192
 
158
193
  protected onAfterAddedToScene(): void {
159
194
  super.onAfterAddedToScene();
160
195
  if (this.shadowComponent) {
196
+ // @TODO: I think we dont even need this anymore and this leads to the offset being applied twice
161
197
  //@ts-ignore
162
198
  this.shadowComponent.offset = this.shadowComponent.position.z;
163
199
 
src/engine-components/ui/Image.ts CHANGED
@@ -24,11 +24,15 @@
24
24
 
25
25
  private _sprite?: Sprite;
26
26
 
27
+ @serializable()
28
+ private pixelsPerUnitMultiplier: number = 1;
29
+
27
30
  private isBuiltinSprite() {
28
31
  switch (this.sprite?.texture?.name) {
29
32
  case "InputFieldBackground":
30
33
  case "UISprite":
31
34
  case "Background":
35
+ case "Knob":
32
36
  return true;
33
37
  }
34
38
  // this is a hack/workaround for production builds where the name of the sprite is missing
@@ -39,12 +43,17 @@
39
43
  }
40
44
 
41
45
  protected onBeforeCreate(opts: any): void {
46
+ super.onBeforeCreate(opts);
42
47
  if (this.isBuiltinSprite()) {
43
- opts.borderRadius = 5;
48
+ opts.borderRadius = 5 / this.pixelsPerUnitMultiplier;
49
+ if(this.sprite?.texture?.name === "Knob") {
50
+ opts.borderRadius = 999;
51
+ }
44
52
  opts.borderColor = new Color(.4, .4, .4);
45
53
  opts.borderOpacity = this.color.alpha;
46
54
  opts.borderWidth = .3;
47
55
  }
56
+
48
57
  }
49
58
 
50
59
  protected onAfterCreated(): void {
src/engine-components/ui/InputField.ts CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  export class InputField extends Behaviour implements IPointerEventHandler {
15
15
 
16
- get text() : string {
16
+ get text(): string {
17
17
  return this.textComponent?.text ?? "";
18
18
  }
19
19
 
@@ -148,6 +148,14 @@
148
148
  this.onEndEdit?.invoke(InputField.htmlField.value);
149
149
  }
150
150
 
151
+ // @Marwie, I can provide this fix. But the issue seems to comes from Raycaster+EventSystem
152
+ // As we rollout InputField, and no others elements is behind raycast,
153
+ // ThreeMeshUI.update is not called.
154
+ update() {
155
+ if (InputField.active === this) {
156
+ this.textComponent?.markDirty();
157
+ }
158
+ }
151
159
 
152
160
  private onInput(evt: KeyboardEvent) {
153
161
  if (InputField.active !== this) return;
src/engine-components/ui/Interfaces.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { IComponent } from "../../engine/engine_types";
2
2
 
3
+ export interface ICanvas {
4
+ get screenspace() : boolean;
5
+ }
6
+
3
7
  export interface ICanvasGroup {
4
8
  get isCanvasGroup() : boolean;
5
9
  blocksRaycasts: boolean;
@@ -9,4 +13,12 @@
9
13
  export interface IGraphic extends IComponent {
10
14
  get isGraphic() : boolean;
11
15
  raycastTarget: boolean;
16
+ }
17
+
18
+ export interface IRectTransform extends IComponent {
19
+
20
+ }
21
+
22
+ export interface IRectTransformChangedReceiver {
23
+ onParentRectTransformChanged(comp : IRectTransform) : void;
12
24
  }
src/engine-components/ui/Keyboard.ts DELETED
@@ -1,204 +0,0 @@
1
- import * as ThreeMeshUI from 'three-mesh-ui'
2
- import * as THREE from 'three'
3
- import { BaseUIComponent, includesDir } from './BaseUIComponent';
4
- import { Text } from './Text';
5
- import { getWorldScale } from '../../engine/engine_three_utils';
6
- import { RectTransform } from './RectTransform';
7
- import { GameObject } from '../Component';
8
-
9
-
10
- enum KeymapOption {
11
- fr,
12
- ru,
13
- de,
14
- es,
15
- el,
16
- nord,
17
- eng
18
- }
19
-
20
-
21
- // see https://github.com/felixmariotto/three-mesh-ui/blob/master/examples/keyboard.js
22
- export class Keyboard extends BaseUIComponent {
23
-
24
- font?: string;
25
- text?: Text;
26
- keymap?: KeymapOption;
27
- padding?: number;
28
- margin?: number;
29
- fontSize?: number;
30
- borderRadius?: number;
31
-
32
-
33
- private colors = {
34
- keyboardBack: 0x858585,
35
- panelBack: 0x262626,
36
- button: 0x363636,
37
- hovered: 0x1c1c1c,
38
- selected: 0x109c5d,
39
- };
40
-
41
-
42
- awake() {
43
- super.awake();
44
- const langKey = KeymapOption[this.keymap || KeymapOption.eng];
45
- this.makeKeyboard(langKey);
46
- }
47
- onEnable(): void {
48
- this.addShadowComponent(this.keyboard);
49
- }
50
- onDisable(): void {
51
- this.removeShadowComponent();
52
- }
53
-
54
- private keyboard: ThreeMeshUI.Keyboard | null = null!;
55
- private _lastKeyPressed: any;
56
- private _lastKeyPressedStartTime: number = 0;
57
- private _lastKeyPressedTime: number = 0;
58
-
59
- private makeKeyboard(language?: string) {
60
-
61
- if (!language && !navigator.language) {
62
- language = "en";
63
- }
64
-
65
- const fontName = this.font ? this.font : "arial";
66
-
67
- const rt = GameObject.getComponent(this.gameObject, RectTransform);
68
- if(!rt){
69
- console.error("Missing rect transform, please add this component inside a canvas");
70
- return;
71
- }
72
- const opts = {
73
- ...rt.getBasicOptions(),
74
- margin: this.margin || 0,
75
- padding: this.padding || 0,
76
- language: language,
77
- fontFamily: includesDir + "/" + fontName + "-msdf.json",
78
- fontTexture: includesDir + "/" + fontName + ".png",
79
- fontSize: this.fontSize || 6, // fontSize will propagate to the keys blocks
80
- backgroundColor: new THREE.Color(this.colors.keyboardBack),
81
- backspaceTexture: includesDir + '/backspace.png',
82
- shiftTexture: includesDir + '/shift.png',
83
- enterTexture: includesDir + '/enter.png',
84
- borderRadius: this.borderRadius || 0,
85
- autoLayout: false,
86
-
87
- };
88
- // const ws = getWorldScale(this.gameObject);
89
- const scale = this.gameObject.scale;
90
- opts.width *= this.gameObject.scale.x;
91
- opts.height *= this.gameObject.scale.y;
92
- opts.fontSize *= Math.max(scale.x, scale.y);
93
- this.keyboard = new ThreeMeshUI.Keyboard(opts);
94
-
95
- // const scale = this.gameObject.scale;
96
- // const max = Math.max(scale.x, scale.y, scale.z);
97
- this.gameObject.scale.set(1, 1, 1);
98
-
99
- //@ts-ignore
100
- this.keyboard.keys.forEach((key) => {
101
-
102
- key.setupState({
103
- state: 'normal',
104
- attributes: {
105
- offset: 0.003,
106
- backgroundColor: new THREE.Color(this.colors.button),
107
- backgroundOpacity: 1
108
- }
109
- });
110
- key.setState("normal");
111
-
112
- key.setupState({
113
- state: 'hovered',
114
- attributes: {
115
- offset: 0.3,
116
- backgroundColor: new THREE.Color(this.colors.hovered),
117
- backgroundOpacity: 1
118
- }
119
- });
120
-
121
- key.setupState({
122
- state: 'pressed',
123
- attributes: {
124
- offset: 0.1,
125
- backgroundColor: new THREE.Color(this.colors.selected),
126
- backgroundOpacity: 1
127
- },
128
- // triggered when the user clicked on a keyboard's key
129
- onSet: () => {
130
- const input = key.info.input;
131
- const cmd = key.info.command;
132
- if (this._lastKeyPressed !== input) {
133
- this._lastKeyPressedStartTime = this.context.time.time;
134
- }
135
- else if (this.context.time.time - this._lastKeyPressedTime > .05) {
136
- // there was probably a key up inbetween
137
- this._lastKeyPressedStartTime = this.context.time.time;
138
- }
139
- else if (this.context.time.time - this._lastKeyPressedStartTime < .5
140
- || cmd == "switch"
141
- || cmd == "shift"
142
- || cmd == "switch-set"
143
- ) {
144
- this._lastKeyPressedTime = this.context.time.time;
145
- return;
146
- }
147
- this._lastKeyPressedTime = this.context.time.time;
148
- this._lastKeyPressed = input;
149
- // if the key have a command (eg: 'backspace', 'switch', 'enter'...)
150
- // special actions are taken
151
- if (cmd) {
152
- switch (cmd) {
153
- // switch between panels
154
- case 'switch':
155
- //@ts-ignore
156
- this.keyboard.setNextPanel();
157
- break;
158
-
159
- // switch between panel charsets (eg: russian/english)
160
- case 'switch-set':
161
- //@ts-ignore
162
- this.keyboard.setNextCharset();
163
- break;
164
-
165
- case 'enter':
166
- this.tryAppend('\n');
167
- break;
168
-
169
- case 'space':
170
- this.tryAppend(' ');
171
- break;
172
-
173
- case 'backspace':
174
- //@ts-ignore
175
- if (!this.text?.text?.length) break
176
- if (this.text?.text)
177
- this.text.text = this.text.text.substring(0, this.text.text.length - 1) || ""
178
- break;
179
-
180
- case 'shift':
181
- //@ts-ignore
182
- this.keyboard.toggleCase();
183
- break;
184
-
185
- };
186
-
187
- // print a glyph, if any
188
- } else if (key.info.input !== undefined) {
189
- this.tryAppend(key.info.input);
190
- };
191
-
192
- }
193
- });
194
-
195
- });
196
- };
197
-
198
- private tryAppend(char: string) {
199
- if (this.text) {
200
- this.text.text += char;
201
- this.markDirty();
202
- }
203
- }
204
- }
src/engine-components/OrbitControls.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3 } from "three";
10
10
  import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls";
11
- import { EventSystem, EventSystemEvents } from "./ui/EventSystem";
11
+ import { AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem";
12
12
  import { ICameraController } from "../engine/engine_types";
13
13
  import { setCameraController } from "../engine/engine_camera";
14
14
  import { SyncedTransform } from "./SyncedTransform";
@@ -176,15 +176,22 @@
176
176
  }
177
177
 
178
178
  private onControlsChangeStarted() {
179
- if(this._syncedTransform) {
179
+ if (this._syncedTransform) {
180
180
  this._syncedTransform.requestOwnership();
181
181
  }
182
182
  }
183
183
 
184
184
  private _shouldDisable: boolean = false;
185
- private afterHandleInput() {
186
- if (this._controls && this._eventSystem) {
187
- this._shouldDisable = this._eventSystem.hasActiveUI;
185
+ private afterHandleInput(evt: CustomEvent<AfterHandleInputEvent>) {
186
+ if (evt.detail.args.pointerId === 0) {
187
+ if (evt.detail.args.isDown) {
188
+ if (this._controls && this._eventSystem) {
189
+ this._shouldDisable = this._eventSystem.hasActiveUI;
190
+ }
191
+ }
192
+ else if (!evt.detail.args.isPressed || evt.detail.args.isUp) {
193
+ this._shouldDisable = false;
194
+ }
188
195
  }
189
196
  }
190
197
 
@@ -205,6 +212,7 @@
205
212
 
206
213
  onBeforeRender() {
207
214
  if (!this._controls) return;
215
+ if (this._cameraObject !== this.context.mainCamera) return;
208
216
 
209
217
  if (this.context.input.getPointerDown(0) || this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2)) {
210
218
  this._inputs += 1;
@@ -267,7 +275,7 @@
267
275
  if (this._controls && !this.context.isInXR) {
268
276
  if (this.debugLog)
269
277
  this._controls.domElement = this.context.renderer.domElement;
270
- this._controls.enabled = !this._shouldDisable;
278
+ this._controls.enabled = !this._shouldDisable && this._camera === this.context.mainCameraComponent;
271
279
  this._controls.update();
272
280
  }
273
281
  }
src/engine-components/ui/RectTransform.ts CHANGED
@@ -1,11 +1,14 @@
1
- import { Behaviour } from "../Component";
2
1
  import * as ThreeMeshUI from 'three-mesh-ui'
3
2
  import { BaseUIComponent } from "./BaseUIComponent";
3
+ import { DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement";
4
4
  import { serializable } from "../../engine/engine_serialization_decorator";
5
- import { Color, Matrix4, Object3D, Vector2, Vector3 } from "three";
5
+ import { Color, Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
6
6
  import { EventSystem } from "./EventSystem";
7
7
  import { getParam } from "../../engine/engine_utils";
8
8
  import { onChange } from "./Utils";
9
+ import { foreachComponentEnumerator } from "../../engine/engine_gameobject";
10
+ import { ICanvas, IRectTransform, IRectTransformChangedReceiver } from "./Interfaces";
11
+ import { GameObject } from '../Component';
9
12
 
10
13
  const debug = getParam("debugui");
11
14
 
@@ -21,38 +24,84 @@
21
24
  height!: number;
22
25
  }
23
26
 
24
- export class RectTransform extends BaseUIComponent {
27
+ const tempVec = new Vector3();
28
+ const tempMatrix = new Matrix4();
29
+ const tempQuaternion = new Quaternion();
25
30
 
26
- offset: number = 0.01;
31
+ export class RectTransform extends BaseUIComponent implements IRectTransform, IRectTransformChangedReceiver {
27
32
 
33
+ offset: number = 0.05;
34
+
28
35
  // @serializable(Object3D)
29
36
  // root? : Object3D;
30
37
 
31
38
  get translation() { return this.gameObject.position; }
32
39
  get rotation() { return this.gameObject.quaternion; }
33
- get scale(): THREE.Vector3 { return this.gameObject.scale; }
40
+ get scale(): Vector3 { return this.gameObject.scale; }
34
41
 
35
42
  private _anchoredPosition!: Vector3;
36
- private get anchoredPosition() {
43
+
44
+ @serializable(Vector3)
45
+ get anchoredPosition() {
37
46
  if (!this._anchoredPosition) this._anchoredPosition = new Vector3();
38
47
  return this._anchoredPosition;
39
48
  }
49
+ private set anchoredPosition(value: Vector3) {
50
+ this._anchoredPosition = value;
51
+ }
40
52
 
41
53
  @serializable(Rect)
42
- rect?: Rect;
54
+ private rect?: Rect; // TODO: should we use the rect or sizeDelta?
55
+
43
56
  @serializable(Vector2)
44
- sizeDelta!: THREE.Vector2;
45
- @serializable(Vector3)
46
- anchoredPosition3D?: THREE.Vector3;
57
+ sizeDelta!: Vector2;
58
+
47
59
  @serializable(Vector2)
48
- pivot?: THREE.Vector2;
60
+ pivot?: Vector2;
49
61
 
62
+ @serializable(Vector2)
63
+ anchorMin!: Vector2;
64
+ @serializable(Vector2)
65
+ anchorMax!: Vector2;
66
+
67
+ @serializable(Vector2)
68
+ offsetMin!: Vector2;
69
+ @serializable(Vector2)
70
+ offsetMax!: Vector2;
71
+
72
+ get width() {
73
+ if (this.anchorMin.x !== this.anchorMax.x) {
74
+ if (this._parentRectTransform) {
75
+ const parentWidth = this._parentRectTransform.width;
76
+ const anchorWidth = this.anchorMax.x - this.anchorMin.x;
77
+ let width = parentWidth * anchorWidth;
78
+ width += this.sizeDelta.x;
79
+ return width;
80
+ }
81
+ }
82
+ return this.sizeDelta.x;
83
+ }
84
+ get height() {
85
+ if (this.anchorMin.y !== this.anchorMax.y) {
86
+ if (this._parentRectTransform) {
87
+ const parentHeight = this._parentRectTransform.height;
88
+ const anchorHeight = this.anchorMax.y - this.anchorMin.y;
89
+ let height = parentHeight * anchorHeight;
90
+ height += this.sizeDelta.y;
91
+ return height
92
+ }
93
+ }
94
+ return this.sizeDelta.y;
95
+ }
96
+
97
+ private lastMatrixWorld!: Matrix4;
50
98
  private lastMatrix!: Matrix4;
51
99
  private rectBlock!: Object3D;
52
100
  private _transformNeedsUpdate: boolean = false;
53
101
 
54
102
  awake() {
55
103
  super.awake();
104
+ this.lastMatrixWorld = new Matrix4();
56
105
  this.lastMatrix = new Matrix4();
57
106
  this.rectBlock = new Object3D();;
58
107
  this.rectBlock.position.z = .1;
@@ -60,7 +109,26 @@
60
109
 
61
110
  // this is required if an animator animated the transform anchoring
62
111
  if (!this._anchoredPosition) this._anchoredPosition = new Vector3();
112
+
113
+ // TODO: we need to replace this with the watch that e.g. Rigibody is using (or the one in utils?)
114
+ // perhaps we can also just manually check the few properties in the update loops?
63
115
  onChange(this, "_anchoredPosition", () => { this._transformNeedsUpdate = true; });
116
+ onChange(this, "sizeDelta", () => { this._transformNeedsUpdate = true; });
117
+ onChange(this, "pivot", () => { this._transformNeedsUpdate = true; });
118
+
119
+ // When exported with an anchored position offset we remove it here
120
+ // because it would otherwise be applied twice when the anchoring is animated
121
+ // Maybe we can get rid of this workaround if we just set the mesh ui position from the
122
+ // anchored position value but then we would have to make sure if a user/the engine updates
123
+ // "position" the change would also land in anchoredPosition
124
+ // Another solution would perhaps be to get rid of the extra "anchoredPosition" vector3
125
+ // and instead use the same vector3 instance on both "position" and "anchoredPosition"
126
+ // But I'm also not sure if this will not cause issues elsewhere later / be confusing?
127
+ // (that being said we can make anchoredPosition hidden)
128
+ if (!this.isRoot()) {
129
+ this.gameObject.position.x += this.anchoredPosition.x;
130
+ this.gameObject.position.y -= this.anchoredPosition.y;
131
+ }
64
132
  }
65
133
 
66
134
  onEnable() {
@@ -74,75 +142,134 @@
74
142
  this.removeShadowComponent();
75
143
  }
76
144
 
145
+ onParentRectTransformChanged(_comp: IRectTransform) {
146
+ // When the parent rect transform changes we have to to recalculate our transform
147
+ this._transformNeedsUpdate = true;
148
+ this.applyTransform();
149
+ }
150
+
151
+ private _parentRectTransform?: RectTransform;
152
+
77
153
  private applyTransform() {
78
154
  const uiobject = this.shadowComponent;
79
155
  if (!uiobject) return;
80
156
  this._transformNeedsUpdate = false;
157
+ this._parentRectTransform = GameObject.getComponentInParent(this.gameObject.parent!, RectTransform) as RectTransform;
81
158
 
159
+ if (debug) console.log("RectTransform ApplyTransform", this.name, this.isRoot());
160
+
82
161
  if (!this.isRoot()) {
83
- // this.gameObject transform has authority over three mesh ui shadow components
84
- // so we keep copy the transform to the threemesh ui components
85
- uiobject.position.copy(this.gameObject.position);
86
- uiobject.position.x *= -1;
87
- uiobject.position.z *= -1;
88
- // move slightly forward to avoid z fighting
89
- uiobject.position.z -= this.offset;
90
-
91
- uiobject.quaternion.copy(this.gameObject.quaternion);
92
- uiobject.rotation.x *= -1;
93
- // flip images
94
- uiobject.rotation.z *= -1;
95
-
96
- uiobject.scale.copy(this.gameObject.scale);
162
+ // Reset temp matrix
163
+ uiobject.matrix.identity();
164
+ uiobject.matrixAutoUpdate = false;
165
+ // calc pivot and apply
166
+ tempVec.set(0, 0, 0);
167
+ this.applyPivot(tempVec);
168
+ uiobject.matrix.setPosition(tempVec.x, tempVec.y, 0);
169
+ // calc rotation matrix and apply (we can skip this if it's not rotated)
170
+ if (this.gameObject.quaternion.x || this.gameObject.quaternion.y || this.gameObject.quaternion.z) {
171
+ tempQuaternion.copy(this.gameObject.quaternion);
172
+ tempQuaternion.x *= -1;
173
+ tempQuaternion.z *= -1;
174
+ tempMatrix.makeRotationFromQuaternion(tempQuaternion);
175
+ uiobject.matrix.premultiply(tempMatrix);
176
+ }
177
+ // calc anchors and offset and apply
178
+ tempVec.set(0, 0, 0);
179
+ this.applyAnchoring(tempVec);
180
+ tempVec.z += this.offset;
181
+ tempVec.z -= this.gameObject.position.z;
182
+ tempMatrix.identity();
183
+ tempMatrix.setPosition(tempVec.x, tempVec.y, tempVec.z);
184
+ uiobject.matrix.premultiply(tempMatrix);
185
+ // apply scale if necessary
186
+ if (this.gameObject.scale.x || this.gameObject.scale.y || this.gameObject.scale.z)
187
+ uiobject.matrix.scale(this.gameObject.scale);
97
188
  }
98
189
  else {
99
- uiobject.rotation.y = Math.PI;
190
+ // We have to rotate the canvas when it's in worldspace
191
+ const canvas = this.Root as any as ICanvas;
192
+ if (!canvas.screenspace) uiobject.rotation.y = Math.PI;
100
193
  }
101
194
 
102
- this.applyAnchoring(uiobject.position);
195
+ this._copyMatrixAfterRender = true;
103
196
  this.lastMatrix.copy(this.gameObject.matrix);
197
+
198
+ // iterate other components on this object that might need to know about the transform change
199
+ // e.g. Graphic components should update their width and height
200
+ const includeChildren = true;
201
+ for (const comp of foreachComponentEnumerator(this.gameObject, BaseUIComponent, includeChildren)) {
202
+ if (comp === this) continue;
203
+ const callback = comp as any as IRectTransformChangedReceiver;
204
+ if (callback.onParentRectTransformChanged)
205
+ callback.onParentRectTransformChanged(this);
206
+ }
104
207
  }
105
208
 
209
+ private _copyMatrixAfterRender: boolean = false;
210
+
106
211
  markDirty() {
107
212
  this._transformNeedsUpdate = true;
108
213
  }
109
214
 
215
+
110
216
  onBeforeRender() {
111
- // only handle update here if this is not the canvas
112
- // the canvas component does inherit from this class but it only serves as a root
113
- // it does not emit any UI elements and therefor we dont want to change its transform
114
- // if (this._parentComponent)
115
- // {
116
- const transformChanged = this._transformNeedsUpdate || this.lastMatrix.equals(this.gameObject.matrix) === false;
117
- if (transformChanged) {
118
- if (debug)
119
- console.log("updating", this.name);
217
+ // TODO: instead of checking matrix again it would perhaps be better to test if position, rotation or scale have changed individually?
218
+ const transformChanged = this.gameObject.matrixWorldNeedsUpdate || this._transformNeedsUpdate || !this.lastMatrixWorld.equals(this.gameObject.matrixWorld) || !this.lastMatrix.equals(this.gameObject.matrix);
219
+ if (transformChanged)
220
+ {
120
221
  this.applyTransform();
121
222
  }
122
- // }
123
- EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
124
223
  }
125
224
 
126
- private applyAnchoring(pos: THREE.Vector3) {
127
- if (this.pivot && this.sizeDelta) {
128
- let tx = (this.pivot.x * 2 - 1);
129
- let ty = (this.pivot.y * 2 - 1);
130
- tx -= this.anchoredPosition.x;// * .05;
131
- ty -= this.anchoredPosition.y;// * .05;
132
- const offx = tx;
133
- const offy = ty;
134
- // console.log(this.name, this.pivot, tx, ty, "offset", offx, offy);
135
- pos.x -= offx;
136
- pos.y -= offy;
137
-
138
- // TODO update size from anchoring, width, height, sizeDelta
139
- if (this.shadowComponent)
140
- // console.log(this.shadowComponent)
141
- this.set({ width: this.sizeDelta.x, height: this.sizeDelta.y });
225
+ onAfterRender() {
226
+ if (this._copyMatrixAfterRender) {
227
+ // can we only have this event when the transform changed in this frame? Otherwise all RectTransforms will be iterated. Not sure what is better
228
+ this.lastMatrixWorld.copy(this.gameObject.matrixWorld);
142
229
  }
143
230
  }
144
231
 
145
- getBasicOptions(): ThreeMeshUI.BlockOptions {
232
+ /** applies the position offset to the passed in vector */
233
+ private applyAnchoring(pos: Vector3) {
234
+ pos.x += this.anchoredPosition.x;
235
+ pos.y += this.anchoredPosition.y;
236
+
237
+ const parent = this._parentRectTransform;
238
+ if (parent) {
239
+ // Calculate vertical offset
240
+ let oy = 0;
241
+ const vert = 1 - this.anchorMax.y - this.anchorMin.y;
242
+ oy -= (parent.height * .5) * vert;
243
+ pos.y += oy;
244
+
245
+ // calculate horizontal offset
246
+ let ox = 0;
247
+ const horz = 1 - this.anchorMax.x - this.anchorMin.x;
248
+ ox -= (parent.width * .5) * horz;
249
+ pos.x += ox;
250
+ }
251
+ }
252
+
253
+ /** applies the pivot offset to the passed in vector */
254
+ private applyPivot(vec: Vector3) {
255
+ if (this.pivot && !this.isRoot()) {
256
+ const pv = this.pivot.x - .5;
257
+ vec.x -= pv * this.sizeDelta.x * this.gameObject.scale.x;
258
+ const ph = this.pivot.y - .5;
259
+ vec.y -= ph * this.sizeDelta.y * this.gameObject.scale.y;
260
+ }
261
+ }
262
+
263
+ getBasicOptions(): ThreeMeshUIEveryOptions {
264
+
265
+ // @TODO : instead of getBasicOptions for each component we could use once needleEngine initialized
266
+ // ThreeMeshUI.DefaultValues.set({
267
+ // backgroundOpacity: 1,
268
+ // borderWidth: 0, // if we dont specify width here a border will automatically propagated to child blocks
269
+ // borderRadius: 0,
270
+ // borderOpacity: 0,
271
+ // })
272
+
146
273
  const opts = {
147
274
  width: this.rect!.width,
148
275
  height: this.rect!.height,// * this.context.mainCameraComponent!.aspect,
@@ -151,6 +278,7 @@
151
278
  borderWidth: 0, // if we dont specify width here a border will automatically propagated to child blocks
152
279
  borderRadius: 0,
153
280
  borderOpacity: 0,
281
+ letterSpacing: -0.03,
154
282
  // justifyContent: 'center',
155
283
  // alignItems: 'center',
156
284
  // alignContent: 'center',
@@ -169,17 +297,32 @@
169
297
  return opts;
170
298
  }
171
299
 
172
- private _createdBlocks : ThreeMeshUI.Block[] = [];
300
+ private _createdBlocks: ThreeMeshUI.Block[] = [];
301
+ private _createdTextBlocks: ThreeMeshUI.Text[] = [];
173
302
 
174
- createNewBlock(opts?: ThreeMeshUI.BlockOptions | object): ThreeMeshUI.Block {
303
+ createNewBlock(opts?: ThreeMeshUIEveryOptions | object): ThreeMeshUI.Block {
175
304
  opts = {
176
305
  ...this.getBasicOptions(),
177
306
  ...opts
178
307
  };
179
308
  if (debug)
180
309
  console.log(this.name, opts);
181
- const block = new ThreeMeshUI.Block(opts as ThreeMeshUI.BlockOptions);
310
+ const block = new ThreeMeshUI.Block(opts as ThreeMeshUIEveryOptions);
182
311
  this._createdBlocks.push(block);
183
312
  return block;
184
313
  }
314
+
315
+ createNewText(opts?: ThreeMeshUIEveryOptions | object): ThreeMeshUI.Block {
316
+ if (debug)
317
+ console.log(opts)
318
+ opts = {
319
+ ...this.getBasicOptions(),
320
+ ...opts,
321
+ };
322
+ if (debug)
323
+ console.log(this.name, opts);
324
+ const block = new ThreeMeshUI.Text(opts as ThreeMeshUIEveryOptions);
325
+ this._createdTextBlocks.push(block);
326
+ return block;
327
+ }
185
328
  }
src/engine-components/ui/Text.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  import { Graphic } from './Graphic';
2
2
  import * as ThreeMeshUI from 'three-mesh-ui'
3
- import { $shadowDomOwner, includesDir } from "./BaseUIComponent";
4
- import { RectTransform } from './RectTransform';
3
+ import { DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement";
5
4
  import { Color } from 'three';
6
- import { FrameEvent } from '../../engine/engine_setup';
7
5
  import { updateRenderSettings } from './Utils';
8
6
  import { Canvas } from './Canvas';
9
7
  import { serializable } from '../../engine/engine_serialization_decorator';
10
8
  import { getParam, resolveUrl } from '../../engine/engine_utils';
9
+ import { ICanvas } from './Interfaces';
11
10
 
12
11
  const debug = getParam("debugtext");
13
12
 
@@ -32,7 +31,7 @@
32
31
  Overflow = 1,
33
32
  }
34
33
 
35
- enum FontStyle {
34
+ export enum FontStyle {
36
35
  Normal = 0,
37
36
  Bold = 1,
38
37
  Italic = 2,
@@ -53,7 +52,7 @@
53
52
  lineSpacing: number = 1;
54
53
  @serializable()
55
54
  supportRichText: boolean = false;
56
- @serializable()
55
+ @serializable(URL)
57
56
  font?: string;
58
57
  @serializable()
59
58
  fontStyle: FontStyle = FontStyle.Normal;
@@ -62,94 +61,108 @@
62
61
  get text(): string {
63
62
  return this._text;
64
63
  }
64
+
65
65
  set text(val: string) {
66
+
67
+
66
68
  this._text = val;
67
- if (!this._textMeshUi && this._text.length > 0 && this.context.time.frame > 0) {
68
- this.createText(val, this.getTextOpts(), this.supportRichText);
69
- }
70
- if (this._textMeshUi) {
71
- if (this._textMeshUi.length > 1) {
72
- this.requestRebuild();
73
- return;
74
- }
75
- //@ts-ignore
76
- this._textMeshUi[0].set({ content: val });
77
- this.markDirty();
78
- }
69
+ this.feedText(this.text, this.supportRichText);
79
70
  }
71
+
80
72
  private set_text(val: string) {
81
73
  this.text = val;
82
74
  }
83
75
 
84
76
  @serializable()
85
- get fontSize(): number { return this._fontSize; }
77
+ get fontSize(): number {
78
+ return this._fontSize;
79
+ }
80
+
86
81
  set fontSize(val: number) {
82
+
83
+ // Setting that kind of property in a parent, would cascade to each 'non-overrided' children.
87
84
  this._fontSize = val;
88
- if (this._textMeshUi) {
89
- if (this._textMeshUi.length > 1) {
90
- this.requestRebuild();
91
- return;
92
- }
93
- //@ts-ignore
94
- this._textMeshUi[0].set({ fontSize: val });
95
- this.markDirty();
96
- }
85
+ this.uiObject?.set({ fontSize: val });
86
+
97
87
  }
98
88
 
89
+
99
90
  protected onColorChanged(): void {
100
- if (this._textMeshUi) {
101
- if (this._textMeshUi.length > 1) {
102
- this.requestRebuild();
103
- return;
104
- }
105
- const col = this.color;
106
- //@ts-ignore
107
- this._textMeshUi[0].set({ fontColor: col, fontOpacity: col.alpha });
108
- this.markDirty();
91
+ this.uiObject?.set({ color: this.color, fontOpacity: this.color.alpha });
92
+ }
93
+
94
+ onParentRectTransformChanged(): void {
95
+ super.onParentRectTransformChanged();
96
+ if (this.uiObject) {
97
+ this.updateOverflow();
109
98
  }
110
99
  }
111
100
 
112
- private _isWaitingForRebuild: boolean = false;
113
- private requestRebuild() {
114
- if (this._isWaitingForRebuild) return;
115
- this._isWaitingForRebuild = true,
116
- this.startCoroutine(this.rebuildDelayedRoutine(), FrameEvent.EarlyUpdate);
101
+ onBeforeRender(): void {
102
+ if (this.uiObject && (this.Root as any as ICanvas).screenspace) {
103
+ this.updateOverflow();
104
+ }
117
105
  }
118
- private *rebuildDelayedRoutine() {
119
- this._isWaitingForRebuild = false;
120
- if (this._textMeshUi) {
121
- for (const text of this._textMeshUi) {
122
- text.removeFromParent();
123
- }
124
- this._textMeshUi.length = 0;
106
+
107
+ private updateOverflow() {
108
+ // HACK: force the text overflow to update
109
+ const overflow = (this.uiObject as any)?._overflow;
110
+ if (overflow) {
111
+ overflow._needsUpdate = true;
112
+ this.markDirty();
125
113
  }
126
- this.createText(this.text, this.getTextOpts(), this.supportRichText);
127
- this.markDirty();
128
114
  }
129
115
 
130
116
  protected onCreate(_opts: any): void {
131
117
  if (debug) console.log(this);
132
- const hideOverflow = this.verticalOverflow == VerticalWrapMode.Truncate && this.horizontalOverflow == HorizontalWrapMode.Wrap;
133
- if (hideOverflow)
118
+
119
+ if (this.horizontalOverflow == HorizontalWrapMode.Overflow) {
120
+ // Only line characters in the textContent (\n,\r\t) would be able to multiline the text
121
+ _opts.whiteSpace = 'pre';
122
+ }
123
+
124
+ if (this.verticalOverflow == VerticalWrapMode.Truncate) {
134
125
  this.context.renderer.localClippingEnabled = true;
126
+ _opts.overflow = 'hidden';
127
+ }
135
128
 
129
+
130
+ // @marwie : this combination is currently KO. See sample "Overflow Overview"
131
+ if (this.horizontalOverflow == HorizontalWrapMode.Overflow && this.verticalOverflow == VerticalWrapMode.Truncate) {
132
+ // This could fix this combination, but would require anchors updates to replace element
133
+ // _opts.width = 'auto';
134
+ }
135
+
136
+
137
+ _opts.lineHeight = this.lineSpacing;
138
+
139
+ // @marwie : Should be fixed. Currently _opts are always fed with :
140
+ // backgroundOpacity : color.opacity
141
+ // backgroundColor : color
142
+ delete _opts.backgroundOpacity;
143
+ delete _opts.backgroundColor;
144
+
145
+ // helper to show bounds of text element
146
+ if (debug) {
147
+ _opts.backgroundColor = 0xff9900;
148
+ _opts.backgroundOpacity = 0.5;
149
+ }
150
+
136
151
  const rt = this.rectTransform;
137
- // this._container = this._textMeshUi;
138
- // every mesh ui component must be inside a block
139
- // images emit nothing but blocks
140
- // this code should probably be moved somewhere else and also handle raw image / anything that emits block (sprite?)
141
- // so we only add extra blocks if the parent doesnt have one yet
142
- // maybe we can just ask the component the text will be added to to not rely on our unity components?
143
- // this can hopefully be removed once this is fixed/improved: https://github.com/felixmariotto/three-mesh-ui/issues/168
144
- this._textContainer = this.uiObject = this.createBlock(rt, hideOverflow, null, true);
145
152
 
153
+ // Texts now support both options, block and inline, and inline has all default to inherit
154
+ _opts = { ..._opts, ...this.getTextOpts() };
146
155
 
147
- this.createText(this.text, this.getTextOpts(), this.supportRichText);
148
- if (this.uiObject) {
149
- //@ts-ignore
150
- // this._container.width += this.fontSize * .333; // avoid word wrapping
156
+ this.getAlignment(_opts);
157
+
158
+ if (debug) {
159
+ _opts.backgroundColor = Math.random() * 0xffffff;
160
+ _opts.backgroundOpacity = 0.1;
151
161
  }
152
- this.uiObject = this.createBlock(rt, hideOverflow, this.uiObject, false);
162
+
163
+ this.uiObject = rt.createNewText(_opts);
164
+ this.feedText(this.text, this.supportRichText);
165
+
153
166
  }
154
167
 
155
168
  onAfterAddedToScene() {
@@ -159,24 +172,25 @@
159
172
 
160
173
  private _text: string = "";
161
174
  private _fontSize: number = 12;
162
- private _textMeshUi: Array<ThreeMeshUI.Text> | null = null;
163
- private _textContainer: ThreeMeshUI.Block | null = null;
164
175
 
176
+ private _textMeshUi: Array<ThreeMeshUI.Inline> | null = null;
177
+
178
+
165
179
  private getTextOpts(): object {
166
180
  let fontSize = this.fontSize;
167
181
  // if (this.canvas) {
168
182
  // fontSize /= this.canvas?.scaleFactor;
169
183
  // }
170
184
 
171
- // @TODO : THH check for changes. fontColor => color?
185
+
172
186
  const textOpts = {
173
- content: this.text,
174
- fontColor: this.color,
187
+ color: this.color,
175
188
  fontOpacity: this.color.alpha,
176
189
  fontSize: fontSize,
177
190
  fontKerning: "normal",
191
+
178
192
  };
179
- this.setFont(textOpts, this.fontStyle);
193
+ this.setFont(textOpts as ThreeMeshUIEveryOptions, this.fontStyle);
180
194
  return textOpts;
181
195
  }
182
196
 
@@ -186,190 +200,128 @@
186
200
  this._didHandleTextRenderOnTop = false;
187
201
  if (this.uiObject) {
188
202
  // @ts-ignore
189
- this.uiObject.onAfterUpdate = () => {
190
- this.updateWidth();
203
+
204
+ // @TODO : Evaluate the need of keeping it anonymous.
205
+ // From v7.x afterUpdate can be removed but requires a reference
206
+ this.uiObject.addAfterUpdate(() => {
191
207
  // We need to update the shadow owner when the text updates
192
208
  // because once the font has loaded we get new children (a new mesh)
193
- // which is the text, it needs to be linked back to this component
209
+ // which is the text, it needs to be linked back to this component
194
210
  // to be properly handled by the EventSystem
195
211
  // since the EventSystem looks for shadow component owners to handle events
196
212
  this.setShadowComponentOwner(this.uiObject);
197
213
  this.markDirty();
198
- };
214
+ });
199
215
  }
200
216
 
201
- setTimeout(()=> this.markDirty(), 10);
217
+ setTimeout(() => this.markDirty(), 10);
202
218
  }
203
219
 
204
- private createBlock(rt: RectTransform, hideOverflow: boolean, content: THREE.Object3D | Array<THREE.Object3D> | null, isTextIntermediate: boolean = false): ThreeMeshUI.Block | null {
205
- //@ts-ignore
206
- const opts: ThreeMeshUI.BlockOptions = {};
220
+ private getAlignment(opts: ThreeMeshUIEveryOptions): ThreeMeshUIEveryOptions {
207
221
 
208
- // @TODO : THH would require update for 7.x
209
- opts.hiddenOverflow = hideOverflow;
222
+ opts.flexDirection = "column";
210
223
 
211
- // @TODO : THH would require update for 7.x
212
- opts.interLine = (this.lineSpacing - 1) * this.fontSize * 1.333;
224
+ switch (this.alignment) {
225
+ case TextAnchor.UpperLeft:
226
+ case TextAnchor.MiddleLeft:
227
+ case TextAnchor.LowerLeft:
228
+ opts.textAlign = "left";
229
+ break;
230
+ case TextAnchor.UpperCenter:
231
+ case TextAnchor.MiddleCenter:
232
+ case TextAnchor.LowerCenter:
233
+ opts.textAlign = "center";
213
234
 
214
- this.getAlignment(opts, isTextIntermediate);
215
- const block = rt.createNewBlock(opts);
216
- // The text block should never write depth to avoid z-fighting
217
- const mat = block["backgroundMaterial"];
218
- if(mat) mat.depthWrite = false;
219
- if (content) {
220
- if (Array.isArray(content)) {
221
- block.add(...content);
222
- } else {
223
- block.add(content);
224
- }
235
+ break;
236
+ case TextAnchor.UpperRight:
237
+ case TextAnchor.MiddleRight:
238
+ case TextAnchor.LowerRight:
239
+ opts.textAlign = "right";
240
+ break;
225
241
  }
226
- return block;
227
- }
228
242
 
229
243
 
230
- private getAlignment(opts: ThreeMeshUI.BlockOptions | any, isTextIntermediate: boolean = false): ThreeMeshUI.BlockOptions {
231
- if (!isTextIntermediate) {
232
- opts.contentDirection = "row";
233
-
234
- switch (this.alignment) {
235
- case TextAnchor.UpperLeft:
236
- case TextAnchor.MiddleLeft:
237
- case TextAnchor.LowerLeft:
238
- opts.textAlign = "left";
239
- break;
240
- case TextAnchor.UpperCenter:
241
- case TextAnchor.MiddleCenter:
242
- case TextAnchor.LowerCenter:
243
- opts.textAlign = "center";
244
-
245
- break;
246
- case TextAnchor.UpperRight:
247
- case TextAnchor.MiddleRight:
248
- case TextAnchor.LowerRight:
249
- opts.textAlign = "right";
250
- break;
251
- }
252
- }
253
-
254
244
  switch (this.alignment) {
255
- // @info ThreeMeshUI remaining alignment : space-between|space-around|"stretch(experimental)"
256
245
  default:
257
246
  case TextAnchor.UpperLeft:
258
247
  case TextAnchor.UpperCenter:
259
248
  case TextAnchor.UpperRight:
260
- opts.justifyContent = "start";
249
+ opts.alignItems = "start";
261
250
  break;
262
251
  case TextAnchor.MiddleLeft:
263
252
  case TextAnchor.MiddleCenter:
264
253
  case TextAnchor.MiddleRight:
265
- opts.justifyContent = "center";
254
+ opts.alignItems = "center";
266
255
  break;
267
256
  case TextAnchor.LowerLeft:
268
257
  case TextAnchor.LowerCenter:
269
258
  case TextAnchor.LowerRight:
270
- opts.justifyContent = "end";
259
+ opts.alignItems = "end";
271
260
  break;
272
261
  }
273
262
 
274
- // @TODO : THH evaluate this is still useful. In case of texts, horizontal alignments are made with textAlign
275
- switch (this.alignment) {
276
- case TextAnchor.UpperLeft:
277
- case TextAnchor.MiddleLeft:
278
- case TextAnchor.LowerLeft:
279
- opts.alignItems = "start";
280
- break;
281
- case TextAnchor.UpperCenter:
282
- case TextAnchor.MiddleCenter:
283
- case TextAnchor.LowerCenter:
284
- opts.alignItems = "center";
285
-
286
- break;
287
- case TextAnchor.UpperRight:
288
- case TextAnchor.MiddleRight:
289
- case TextAnchor.LowerRight:
290
- opts.alignItems = "end";
291
- break;
292
- }
293
263
  return opts;
294
264
  }
295
265
 
296
- private updateWidth() {
297
- if (this.horizontalOverflow === HorizontalWrapMode.Overflow) {
298
- setTimeout(() => {
299
- if (!this._textMeshUi) return;
300
- const container = this._textMeshUi[0].parent;
301
- if (!container) return;
302
- //@ts-ignore
303
- if (container.lines) {
304
- //@ts-ignore
305
- let newWidth = container.lines.reduce((accu, line) => { return accu + line.width }, 0);
306
- //@ts-ignore
307
- newWidth += container.getFontSize() * 5;
308
- //@ts-ignore
309
- newWidth += (container.padding * 2 || 0);
310
- newWidth += this.fontSize * 1.5;
311
- // TODO: handle alignment!
312
- // const pos = container.position;
313
- // pos.x = this.gameObject.position.x * -.01 + newWidth * .5 - this.rect.sizeDelta.x * .005;
314
- // this._textMeshUi.set({ position: pos });
315
- //@ts-ignore
316
- container.set({ width: newWidth });
317
- this.ensureShadowComponentOwner();
318
- }
319
- }, 1);
320
- }
321
- }
266
+ private feedText(text: string, richText: boolean) {
267
+ // if (!text || text.length <= 0) return;
268
+ // if (!text ) return;
322
269
 
323
- private ensureShadowComponentOwner() {
324
- if (this.shadowComponent) {
325
- this.shadowComponent.traverse(c => {
326
- if (c[$shadowDomOwner] === undefined)
327
- c[$shadowDomOwner] = this;
328
- });
329
- }
330
- }
331
-
332
- private createText(text: string, opts: any, richText: boolean) {
333
- if (!text || text.length <= 0) return;
270
+ if (!this.uiObject) return null;
334
271
  if (!this._textMeshUi)
335
272
  this._textMeshUi = [];
336
- if (!richText) {
337
- // console.log(text)
338
- const opt = { ...opts };
339
- opt.content = text;
340
- const element = new ThreeMeshUI.Text(opt);
341
- this._textMeshUi.push(element);
342
- if (this._textContainer) {
343
- this._textContainer.add(element);
344
- }
345
- }
346
- else {
273
+
274
+ // this.uiObject.textContent = text;
275
+ // return ;
276
+
277
+ if (!richText || text.length === 0) {
278
+ //@TODO: @swingingtom how would the text content be set?
279
+ //@ts-ignore
280
+ this.uiObject.textContent = text;
281
+ } else {
282
+
283
+
347
284
  let currentTag: TagInfo | null = this.getNextTag(text);
348
285
  if (!currentTag) {
349
- return this.createText(text, opts, false);
286
+ //@TODO: @swingingtom how would the text content be set?
287
+ //@ts-ignore
288
+ return this.uiObject.textContent = text;
289
+ } else if (currentTag.startIndex > 0) {
290
+ this.uiObject.add(new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' }))
350
291
  }
351
- else if (currentTag.startIndex > 0) {
352
- this.createText(text.substring(0, currentTag.startIndex), opts, false);
353
- }
354
292
  const stackArray: Array<TagStackEntry> = [];
355
293
  while (currentTag) {
356
294
  const next = this.getNextTag(text, currentTag.endIndex);
295
+
296
+ const opts = {
297
+ fontFamily: this.uiObject?.get('fontFamily'),
298
+ color: 'inherit',
299
+ textContent: ""
300
+ }
301
+
357
302
  if (next) {
358
- const content = this.getText(text, currentTag, next);
303
+
304
+ opts.textContent = this.getText(text, currentTag, next);
305
+
359
306
  this.handleTag(currentTag, opts, stackArray);
360
- this.createText(content, opts, false);
361
- }
362
- else {
363
- const content = text.substring(currentTag.endIndex);
307
+ this.uiObject?.add(new ThreeMeshUI.Inline(opts))
308
+
309
+ } else {
310
+
311
+ opts.textContent = text.substring(currentTag.endIndex);
312
+
364
313
  this.handleTag(currentTag, opts, stackArray);
365
- this.createText(content, opts, false);
314
+ this.uiObject?.add(new ThreeMeshUI.Inline(opts))
366
315
  }
367
316
  currentTag = next;
368
317
  }
369
318
  }
319
+
320
+ return null;
370
321
  }
371
322
 
372
323
  private _didHandleTextRenderOnTop: boolean = false;
324
+
373
325
  private handleTextRenderOnTop() {
374
326
  if (this._didHandleTextRenderOnTop) return;
375
327
  this._didHandleTextRenderOnTop = true;
@@ -377,11 +329,17 @@
377
329
  }
378
330
 
379
331
  // waits for all the text objects to be ready to set the render on top setting
380
- private *renderOnTopCoroutine() {
332
+ // @THH : this isn't true anymore. We can set mesh and material properties before their counterparts are created.
333
+ // Values would automatically be passed when created. Not sure for depthWrite but it can be added;
334
+ private * renderOnTopCoroutine() {
381
335
  if (!this.canvas) return;
382
336
  const updatedRendering: boolean[] = [];
383
337
  const canvas = this.canvas;
384
- const settings = { renderOnTop: canvas.renderOnTop, depthWrite: canvas.depthWrite, doubleSided: canvas.doubleSided };
338
+ const settings = {
339
+ renderOnTop: canvas.renderOnTop,
340
+ depthWrite: canvas.depthWrite,
341
+ doubleSided: canvas.doubleSided
342
+ };
385
343
  while (true) {
386
344
  let isWaitingForElementToUpdate = false;
387
345
  if (this._textMeshUi) {
@@ -407,36 +365,31 @@
407
365
  // console.log(tag);
408
366
  if (!tag.isEndTag) {
409
367
  if (tag.type.includes("color")) {
410
- const stackEntry = new TagStackEntry(tag, { fontColor: opts.fontColor });
368
+ const stackEntry = new TagStackEntry(tag, { color: opts.color });
411
369
  stackArray.push(stackEntry);
412
370
  if (tag.type.length > 6) // color=
413
371
  {
414
- const col = tag.type.substring(6);
415
- opts.fontColor = new Color(col);
416
- }
417
- else {
372
+ const col = parseInt("0x" + tag.type.substring(7));
373
+ opts.color = col;
374
+ } else {
418
375
  // if it does not contain a color it is white
419
- opts.fontColor = new Color(1, 1, 1);
376
+ opts.color = new Color(1, 1, 1);
420
377
  }
421
- }
422
- else if (tag.type == "b") {
378
+ } else if (tag.type == "b") {
379
+ this.setFont(opts, FontStyle.Bold);
423
380
  const stackEntry = new TagStackEntry(tag, {
424
- fontFamily: opts.fontFamily,
425
- fontTexture: opts.fontTexture,
381
+ fontWeight: 700,
426
382
  });
427
383
  stackArray.push(stackEntry);
428
- this.setFont(opts, FontStyle.Bold);
429
- }
430
- else if (tag.type == "i") {
384
+ } else if (tag.type == "i") {
385
+ this.setFont(opts, FontStyle.Italic);
431
386
  const stackEntry = new TagStackEntry(tag, {
432
- fontFamily: opts.fontFamily,
433
- fontTexture: opts.fontTexture,
387
+ fontStyle: 'italic'
434
388
  });
435
389
  stackArray.push(stackEntry);
436
- this.setFont(opts, FontStyle.Italic);
390
+
437
391
  }
438
- }
439
- else {
392
+ } else {
440
393
  if (stackArray.length > 0) {
441
394
  const last = stackArray.pop();
442
395
  if (last) {
@@ -464,60 +417,126 @@
464
417
  return null;
465
418
  }
466
419
 
467
- private setFont(opts: any, fontStyle: FontStyle) {
468
- const name = this.getFontStyleName(fontStyle);
469
- let family = name;
470
- if (!family?.endsWith("-msdf.json")) family += "-msdf.json";
471
- opts.fontFamily = family;
420
+ /**
421
+ * Update provided opts to have a proper fontDefinition : family+weight+style
422
+ * Ensure Family and Variant are registered in FontLibrary
423
+ *
424
+ * @param opts
425
+ * @param fontStyle
426
+ * @private
427
+ */
428
+ private setFont(opts: ThreeMeshUIEveryOptions, fontStyle: FontStyle) {
472
429
 
473
- let texture = name;
474
- if (!texture?.endsWith(".png")) texture += ".png";
475
- opts.fontTexture = texture;
476
- }
430
+ // @TODO : THH could be useful to uniformize font family name :
431
+ // This would ease possible html/vr matching
432
+ // - Arial instead of assets/arial
433
+ // - Arial should stay Arial instead of arial
434
+ if (!this.font) return;
435
+ let familyName = this.font;
477
436
 
478
- private getFontStyleName(style: FontStyle): string | null {
479
- if (!this.font) return null;
480
- let fontName = this.font;
437
+ // ensure a font family is register under this name
438
+ let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(familyName as string);
439
+ if (!fontFamily)
440
+ fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(familyName as string);
481
441
 
482
- // if a font path has a known suffix we remove it
483
- const fontNameLower = fontName.toLowerCase();
484
- if (fontNameLower.endsWith("-regular")) {
485
- if (style === FontStyle.Normal) return resolveUrl(this.sourceId, fontName);
486
- fontName = fontName.substring(0, fontName.length - "-regular".length);
487
- }
488
- else if (fontNameLower.endsWith("-bold")) {
489
- if (style === FontStyle.Bold)return resolveUrl(this.sourceId, fontName);
490
- fontName = fontName.substring(0, fontName.length - "-bold".length);
491
- }
492
- else if (fontNameLower.endsWith("-italic")) {
493
- if (style === FontStyle.Italic)return resolveUrl(this.sourceId, fontName);
494
- fontName = fontName.substring(0, fontName.length - "-italic".length);
495
- }
496
- else if (fontNameLower.endsWith("-bolditalic")) {
497
- if (style === FontStyle.BoldAndItalic)return resolveUrl(this.sourceId, fontName);
498
- fontName = fontName.substring(0, fontName.length - "-bolditalic".length);
499
- }
500
- else
501
- // If a font does not have a specific style suffic we dont support getting the correct font style
502
- return resolveUrl(this.sourceId, fontName);
442
+ // @TODO: @swingingtom how should the font be set?
443
+ //@ts-ignore
444
+ opts.fontFamily = fontFamily;
445
+ const lowerFamilyName = familyName.toLowerCase();
503
446
 
504
- switch (style) {
447
+ switch (fontStyle) {
448
+ default:
505
449
  case FontStyle.Normal:
506
- fontName += "-regular";
507
- break;
450
+ opts.fontWeight = 400;
451
+ opts.fontStyle = "normal";
452
+ break
453
+
508
454
  case FontStyle.Bold:
509
- fontName += "-bold";
455
+ opts.fontWeight = 700;
456
+ opts.fontStyle = "normal";
457
+ if (!lowerFamilyName.includes("-bold"))
458
+ familyName += "-bold";
510
459
  break;
460
+
511
461
  case FontStyle.Italic:
512
- fontName += "-italic";
462
+ opts.fontWeight = 400;
463
+ opts.fontStyle = "italic"
464
+ if (!lowerFamilyName.includes("-italic"))
465
+ familyName += "-italic";
513
466
  break;
467
+
514
468
  case FontStyle.BoldAndItalic:
515
- fontName += "-bolditalic";
516
- break;
469
+ opts.fontStyle = 'italic';
470
+ opts.fontWeight = 400;
471
+ if (!lowerFamilyName.includes("-bold"))
472
+ familyName += "-bold";
473
+ if (!lowerFamilyName.includes("-italic"))
474
+ familyName += "-italic";
517
475
  }
518
476
 
519
- return resolveUrl(this.sourceId, fontName);
477
+
478
+ // Ensure a fontVariant is registered
479
+ //@TODO: @swingingtom add type for fontWeight
480
+ let fontVariant = fontFamily.getVariant(opts.fontWeight as any as string, opts.fontStyle);
481
+ if (!fontVariant) {
482
+ let jsonPath = familyName;
483
+ if (!jsonPath?.endsWith("-msdf.json")) jsonPath += "-msdf.json";
484
+ let texturePath = familyName;
485
+ if (!texturePath?.endsWith(".png")) texturePath += ".png";
486
+
487
+ //@TODO: @swingingtom add type for fontWeight
488
+ //@TODO: @swingingtom addVariant return type is wrong (should be FontVariant)
489
+ fontVariant = fontFamily.addVariant(opts.fontWeight as any as string, opts.fontStyle, jsonPath, texturePath as string) as any as ThreeMeshUI.FontVariant;
490
+ fontVariant?.addEventListener('ready', () => {
491
+ this.markDirty();
492
+ });
493
+ }
494
+
520
495
  }
496
+
497
+ // private getFontStyleName(style: FontStyle): string | null {
498
+ // if (!this.font) return null;
499
+ // let fontName = this.font;
500
+
501
+ // // if a font path has a known suffix we remove it
502
+ // const fontNameLower = fontName.toLowerCase();
503
+ // if (fontNameLower.endsWith("-regular")) {
504
+ // if (style === FontStyle.Normal) return resolveUrl(this.sourceId, fontName);
505
+ // fontName = fontName.substring(0, fontName.length - "-regular".length);
506
+ // }
507
+ // else if (fontNameLower.endsWith("-bold")) {
508
+ // if (style === FontStyle.Bold) return resolveUrl(this.sourceId, fontName);
509
+ // fontName = fontName.substring(0, fontName.length - "-bold".length);
510
+ // }
511
+ // else if (fontNameLower.endsWith("-italic")) {
512
+ // if (style === FontStyle.Italic) return resolveUrl(this.sourceId, fontName);
513
+ // fontName = fontName.substring(0, fontName.length - "-italic".length);
514
+ // }
515
+ // else if (fontNameLower.endsWith("-bolditalic")) {
516
+ // if (style === FontStyle.BoldAndItalic) return resolveUrl(this.sourceId, fontName);
517
+ // fontName = fontName.substring(0, fontName.length - "-bolditalic".length);
518
+ // }
519
+ // else
520
+ // // If a font does not have a specific style suffic we dont support getting the correct font style
521
+ // return resolveUrl(this.sourceId, fontName);
522
+
523
+ // switch (style) {
524
+ // case FontStyle.Normal:
525
+ // fontName += "-regular";
526
+ // break;
527
+ // case FontStyle.Bold:
528
+ // fontName += "-bold";
529
+ // break;
530
+ // case FontStyle.Italic:
531
+ // fontName += "-italic";
532
+ // break;
533
+ // case FontStyle.BoldAndItalic:
534
+ // fontName += "-bolditalic";
535
+ // break;
536
+ // }
537
+
538
+ // return resolveUrl(this.sourceId, fontName);
539
+ // }
521
540
  }
522
541
 
523
542
  class TagStackEntry {
src/engine-components/TransformGizmo.ts CHANGED
@@ -9,36 +9,50 @@
9
9
  export class TransformGizmo extends Behaviour {
10
10
 
11
11
  @serializable()
12
- public isGizmo: boolean = true;
12
+ public isGizmo: boolean = false;
13
13
 
14
+ @serializable()
15
+ public translationSnap: number = 1;
16
+
17
+ @serializable()
18
+ public rotationSnapAngle: number = 15;
19
+
20
+ @serializable()
21
+ public scaleSnap: number = .25;
22
+
14
23
  private control?: TransformControls;
15
24
  private orbit?: OrbitControls;
16
25
 
17
- awake() {
26
+ onEnable() {
18
27
  if (this.isGizmo && !params.showGizmos) return;
28
+
19
29
  if (!this.context.mainCamera) return;
20
- this.control = new TransformControls(this.context.mainCamera, this.context.renderer.domElement);
21
- this.control.visible = true;
22
- this.control.enabled = true;
23
- this.control.getRaycaster().layers.set(2);
24
30
 
25
- this.control.size = 0.6;
26
- this.control.traverse(x => {
27
- const mesh = x as Mesh;
28
- mesh.layers.set(2);
29
- if (mesh) {
30
- const gizmoMat = mesh.material as THREE.MeshBasicMaterial;
31
- if (gizmoMat) {
32
- gizmoMat.opacity = 0.3;
31
+ if (!this.control) {
32
+ this.control = new TransformControls(this.context.mainCamera, this.context.domElement);
33
+ this.control.visible = true;
34
+ this.control.enabled = true;
35
+ this.control.getRaycaster().layers.set(2);
36
+ this.control.size = 1;
37
+ this.control.traverse(x => {
38
+ const mesh = x as Mesh;
39
+ mesh.layers.set(2);
40
+ if (mesh) {
41
+ const gizmoMat = mesh.material as THREE.MeshBasicMaterial;
42
+ if (gizmoMat) {
43
+ gizmoMat.opacity = 0.3;
44
+ }
33
45
  }
34
- }
35
- });
36
- }
46
+ });
47
+ this.orbit = GameObject.getComponentInParent(this.context.mainCamera, OrbitControls) ?? undefined;
48
+ }
37
49
 
38
- start() {
39
- if (this.context.mainCamera) {
40
- const orbit = GameObject.getComponentInParent(this.context.mainCamera, OrbitControls) ?? undefined;
41
- this.orbit = orbit;
50
+ if (this.control) {
51
+ this.context.scene.add(this.control);
52
+ this.control.attach(this.gameObject);
53
+ this.changeEventListener = this.onControlChangedEvent.bind(this);
54
+ this.control?.addEventListener('dragging-changed', this.changeEventListener);
55
+ this.addWindowEvents();
42
56
  }
43
57
  }
44
58
 
@@ -46,22 +60,29 @@
46
60
  private windowKeyDownListener?: any;
47
61
  private windowKeyUpListener?: any;
48
62
 
49
- onEnable() {
50
- if (this.control) {
51
- this.context.scene.add(this.control);
52
- this.control.attach(this.gameObject);
53
- }
54
- this.changeEventListener = this.onControlChangedEvent.bind(this);
55
- this.control?.addEventListener('dragging-changed', this.changeEventListener);
56
- this.attachWindowEvents();
57
- }
58
-
59
63
  onDisable() {
60
64
  this.control?.removeFromParent();
61
65
  if (this.changeEventListener)
62
66
  this.control?.removeEventListener('dragging-changed', this.changeEventListener);
67
+ this.removeWindowEvents();
63
68
  }
64
69
 
70
+ enableSnapping() {
71
+ if (this.control) {
72
+ this.control.setTranslationSnap(this.translationSnap);
73
+ this.control.setRotationSnap(MathUtils.degToRad(this.rotationSnapAngle));
74
+ this.control.setScaleSnap(this.scaleSnap);
75
+ }
76
+ }
77
+
78
+ disableSnapping() {
79
+ if (this.control) {
80
+ this.control.setTranslationSnap(null);
81
+ this.control.setRotationSnap(null);
82
+ this.control.setScaleSnap(null);
83
+ }
84
+ }
85
+
65
86
  private onControlChangedEvent(event) {
66
87
  const orbit = this.orbit;
67
88
  if (orbit) orbit.enabled = !event.value;
@@ -74,12 +95,13 @@
74
95
  }
75
96
  }
76
97
 
77
- private attachWindowEvents() {
98
+ private addWindowEvents() {
78
99
  const control = this.control;
79
100
  if (!control) return;
80
101
 
81
102
  if (!this.windowKeyDownListener) {
82
103
  this.windowKeyDownListener = (event) => {
104
+ if (!this.enabled) return;
83
105
  switch (event.keyCode) {
84
106
 
85
107
  case 81: // Q
@@ -87,9 +109,7 @@
87
109
  break;
88
110
 
89
111
  case 16: // Shift
90
- control.setTranslationSnap(100);
91
- control.setRotationSnap(MathUtils.degToRad(15));
92
- control.setScaleSnap(0.25);
112
+ this.enableSnapping();
93
113
  break;
94
114
 
95
115
  case 87: // W
@@ -103,34 +123,6 @@
103
123
  case 82: // R
104
124
  control.setMode('scale');
105
125
  break;
106
-
107
- /*
108
- case 67: // C
109
- const position = currentCamera.position.clone();
110
-
111
- currentCamera = currentCamera.isPerspectiveCamera ? cameraOrtho : cameraPersp;
112
- currentCamera.position.copy( position );
113
-
114
- orbit.object = currentCamera;
115
- control.camera = currentCamera;
116
-
117
- currentCamera.lookAt( orbit.target.x, orbit.target.y, orbit.target.z );
118
- onWindowResize();
119
- break;
120
-
121
- case 86: // V
122
- const randomFoV = Math.random() + 0.1;
123
- const randomZoom = Math.random() + 0.1;
124
-
125
- cameraPersp.fov = randomFoV * 160;
126
- cameraOrtho.bottom = - randomFoV * 500;
127
- cameraOrtho.top = randomFoV * 500;
128
-
129
- cameraPersp.zoom = randomZoom * 5;
130
- cameraOrtho.zoom = randomZoom * 5;
131
- onWindowResize();
132
- break;
133
- */
134
126
  case 187:
135
127
  case 107: // +, =, num+
136
128
  control.setSize(control.size + 0.1);
@@ -164,13 +156,10 @@
164
156
 
165
157
  if (!this.windowKeyUpListener) {
166
158
  this.windowKeyUpListener = (event) => {
167
-
159
+ if (!this.enabled) return;
168
160
  switch (event.keyCode) {
169
-
170
161
  case 16: // Shift
171
- control.setTranslationSnap(null);
172
- control.setRotationSnap(null);
173
- control.setScaleSnap(null);
162
+ this.disableSnapping();
174
163
  break;
175
164
 
176
165
  }
@@ -178,8 +167,13 @@
178
167
  };
179
168
  }
180
169
 
181
-
170
+
182
171
  window.addEventListener('keydown', this.windowKeyDownListener);
183
172
  window.addEventListener('keyup', this.windowKeyUpListener);
184
173
  }
174
+
175
+ private removeWindowEvents() {
176
+ window.removeEventListener('keydown', this.windowKeyDownListener);
177
+ window.removeEventListener('keyup', this.windowKeyUpListener);
178
+ }
185
179
  }
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -1,21 +1,20 @@
1
1
  import { WebXR } from "./WebXR";
2
2
  import { serializable } from "../../engine/engine_serialization";
3
- import { Behaviour } from "../Component";
4
- import { Matrix4, Object3D, Quaternion, Vector, Vector3 } from "three";
3
+ import { Behaviour, GameObject } from "../Component";
4
+ import { Object3D, Quaternion, Vector3 } from "three";
5
5
  import { CircularBuffer, getParam } from "../../engine/engine_utils";
6
+ import { AssetReference } from "../../engine/engine_addressables";
6
7
 
7
8
  // https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
8
9
 
9
10
  const debug = getParam("debugimagetracking");
10
11
 
11
- const _scaleTemp = new Vector3();
12
-
13
12
  export class WebXRTrackedImage {
14
13
 
15
-
16
14
  get url(): string { return this._trackedImage.image ?? ""; }
17
15
  get widthInMeters() { return this._trackedImage.widthInMeters ?? undefined; }
18
16
  get bitmap(): ImageBitmap { return this._bitmap; }
17
+ get model(): WebXRImageTrackingModel { return this._trackedImage; }
19
18
  readonly measuredSize: number;
20
19
  readonly state: "tracked" | "emulated";
21
20
 
@@ -64,7 +63,7 @@
64
63
  }
65
64
  }
66
65
 
67
- private readonly _trackingComponent: WebXRImageTracking;;
66
+ private readonly _trackingComponent: WebXRImageTracking;
68
67
  private readonly _trackedImage: WebXRImageTrackingModel;
69
68
  private readonly _bitmap: ImageBitmap;
70
69
  private readonly _pose: any;
@@ -90,6 +89,14 @@
90
89
  @serializable()
91
90
  widthInMeters!: number;
92
91
 
92
+ @serializable(AssetReference)
93
+ object?: AssetReference;
94
+
95
+ @serializable()
96
+ createObjectInstance: boolean = false;
97
+
98
+ @serializable()
99
+ imageDoesNotMove: boolean = false;
93
100
  }
94
101
 
95
102
  export class WebXRImageTracking extends Behaviour {
@@ -97,9 +104,6 @@
97
104
  @serializable(WebXRImageTrackingModel)
98
105
  trackedImages!: WebXRImageTrackingModel[];
99
106
 
100
-
101
-
102
-
103
107
  private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
104
108
 
105
109
  private static _imageElements: Map<string, ImageBitmap | null> = new Map();
@@ -125,13 +129,16 @@
125
129
 
126
130
  onEnable(): void {
127
131
  WebXR.addEventListener("modify-ar-options", this.onModifyAROptions);
132
+ WebXR.addEventListener("xrStarted", this.onXRStarted);
133
+ this.addEventListener("image-tracking", this.onImageTrackingUpdate)
128
134
  }
129
135
 
130
136
  onDisable(): void {
131
137
  WebXR.removeEventListener("modify-ar-options", this.onModifyAROptions);
138
+ WebXR.removeEventListener("xrStarted", this.onXRStarted);
139
+ this.removeEventListener("image-tracking", this.onImageTrackingUpdate)
132
140
  }
133
141
 
134
-
135
142
  private onModifyAROptions = (event: any) => {
136
143
  const options = event.detail;
137
144
  const features = options.optionalFeatures || [];
@@ -154,6 +161,74 @@
154
161
  }
155
162
  }
156
163
 
164
+ private imageToObjectMap: Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number }> = new Map();
165
+
166
+ private onImageTrackingUpdate = (event: any) => {
167
+ const images = event.detail as WebXRTrackedImage[];
168
+
169
+ // disable any objects that are no longer tracked
170
+ for (const [model, object] of this.imageToObjectMap) {
171
+ if (!object.object || !model) continue;
172
+ let found = false;
173
+ for (const trackedImage of images) {
174
+ if (trackedImage.model === model) {
175
+ found = true;
176
+ break;
177
+ }
178
+ }
179
+ if (!found) {
180
+ GameObject.setActive(object.object, false);
181
+ }
182
+ }
183
+
184
+ for (const image of images) {
185
+ const model = image.model;
186
+ // don't do anything if we don't have an object to track - can be handled externally through events
187
+ if (!model.object) continue;
188
+
189
+ let trackedData = this.imageToObjectMap.get(model);
190
+ if (trackedData === undefined) {
191
+ trackedData = { object: null, frames: 0 };
192
+ this.imageToObjectMap.set(model, trackedData);
193
+
194
+ model.object.loadAssetAsync().then((asset: GameObject | null) => {
195
+ if (model.createObjectInstance)
196
+ asset = GameObject.instantiate(asset);
197
+
198
+ if (asset) {
199
+ trackedData!.object = asset;
200
+ if (asset !== this.gameObject)
201
+ this.gameObject.add(asset);
202
+ image.applyToObject(asset);
203
+ if (!asset.activeSelf)
204
+ GameObject.setActive(asset, true);
205
+ }
206
+ });
207
+ }
208
+ else {
209
+ trackedData.frames++;
210
+
211
+ // TODO we could do a bit more here: e.g. sample for the first 1s or so of getting pose data
212
+ // to improve the tracking quality a bit.
213
+ if (model.imageDoesNotMove && trackedData.frames > 10)
214
+ continue;
215
+
216
+ if (!trackedData.object) continue;
217
+
218
+ image.applyToObject(trackedData.object);
219
+ if (!trackedData.object.activeSelf)
220
+ GameObject.setActive(trackedData.object, true);
221
+ }
222
+ }
223
+ }
224
+
225
+ private onXRStarted = (_: any) => {
226
+ // clear out all frame counters for tracking
227
+ for (const trackedData of this.imageToObjectMap.values()) {
228
+ trackedData.frames = 0;
229
+ }
230
+ };
231
+
157
232
  onBeforeRender(frame: XRFrame | null): void {
158
233
  //@ts-ignore
159
234
  if (frame?.session && typeof frame.getImageTrackingResults === "function") {
src/engine-components/utils/LookAt.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { serializable } from "../../engine/engine_serialization";
2
+ import { Behaviour } from "../Component";
3
+ import { Object3D } from "three";
4
+ import { getWorldPosition, lookAtInverse } from "../../engine/engine_three_utils";
5
+
6
+ export class LookAt extends Behaviour {
7
+
8
+ @serializable(Object3D)
9
+ target?: Object3D;
10
+
11
+ @serializable()
12
+ invertForward: boolean = false;
13
+
14
+ onBeforeRender(): void {
15
+ if (!this.target) return;
16
+ if (!this.invertForward)
17
+ this.gameObject.lookAt(getWorldPosition(this.target!));
18
+ else
19
+ lookAtInverse(this.gameObject, getWorldPosition(this.target!));
20
+ }
21
+ }
src/engine-components/ui/Outline.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { RGBAColor } from "../js-extensions";
2
+ import { serializable } from "../../engine/engine_serialization";
3
+ import { Behaviour } from "../Component";
4
+ import { Color, Vector2 } from "three"
5
+
6
+ export class Outline extends Behaviour {
7
+
8
+ @serializable(RGBAColor)
9
+ effectColor?: RGBAColor;
10
+
11
+ @serializable(Vector2)
12
+ effectDistance?: Vector2;
13
+ }