@@ -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);
|
@@ -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
|
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);
|
@@ -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.
|
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.
|
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.
|
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.
|
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
|
-
|
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);
|
@@ -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() {
|
31
|
-
|
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
|
-
|
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
|
-
|
117
|
-
if (this.
|
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
|
-
|
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.
|
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
|
164
|
-
|
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
|
-
|
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
|
-
|
173
|
-
|
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
|
-
|
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 = -
|
240
|
+
canvas.position.z = -plane;
|
241
|
+
canvas.quaternion.identity();
|
182
242
|
|
183
|
-
|
243
|
+
const rect = this.gameObject.getComponent(RectTransform)!;
|
184
244
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
}
|
@@ -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";
|
@@ -27,8 +27,8 @@
|
|
27
27
|
|
28
28
|
private _assetReferences: { [key: string]: AssetReference } = {};
|
29
29
|
|
30
|
-
findAssetReference(
|
31
|
-
return this._assetReferences[
|
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,
|
52
|
-
const fullPath = resolveUrl(sourceId,
|
53
|
-
if (debug) console.log("GetOrCreate Addressable from", sourceId,
|
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(
|
104
|
-
if (
|
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
|
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
|
|
@@ -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
|
}
|
@@ -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) {
|
@@ -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);
|
@@ -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
|
}
|
@@ -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: " +
|
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
|
-
|
568
|
-
|
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
|
}
|
@@ -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
|
-
|
31
|
-
|
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
|
-
|
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({
|
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
|
|
@@ -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 {
|
@@ -13,7 +13,7 @@
|
|
13
13
|
|
14
14
|
export class InputField extends Behaviour implements IPointerEventHandler {
|
15
15
|
|
16
|
-
get text()
|
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;
|
@@ -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
|
}
|
@@ -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
|
-
}
|
@@ -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 (
|
187
|
-
|
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
|
}
|
@@ -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
|
-
|
27
|
+
const tempVec = new Vector3();
|
28
|
+
const tempMatrix = new Matrix4();
|
29
|
+
const tempQuaternion = new Quaternion();
|
25
30
|
|
26
|
-
|
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():
|
40
|
+
get scale(): Vector3 { return this.gameObject.scale; }
|
34
41
|
|
35
42
|
private _anchoredPosition!: Vector3;
|
36
|
-
|
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!:
|
45
|
-
|
46
|
-
anchoredPosition3D?: THREE.Vector3;
|
57
|
+
sizeDelta!: Vector2;
|
58
|
+
|
47
59
|
@serializable(Vector2)
|
48
|
-
pivot?:
|
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
|
-
//
|
84
|
-
|
85
|
-
uiobject.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
uiobject.
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
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.
|
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
|
-
//
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
127
|
-
if (this.
|
128
|
-
|
129
|
-
|
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
|
-
|
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
|
300
|
+
private _createdBlocks: ThreeMeshUI.Block[] = [];
|
301
|
+
private _createdTextBlocks: ThreeMeshUI.Text[] = [];
|
173
302
|
|
174
|
-
createNewBlock(opts?:
|
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
|
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
|
}
|
@@ -1,13 +1,12 @@
|
|
1
1
|
import { Graphic } from './Graphic';
|
2
2
|
import * as ThreeMeshUI from 'three-mesh-ui'
|
3
|
-
import {
|
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
|
-
|
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 {
|
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
|
-
|
89
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
this.
|
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
|
-
|
133
|
-
if (
|
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.
|
148
|
-
|
149
|
-
|
150
|
-
|
156
|
+
this.getAlignment(_opts);
|
157
|
+
|
158
|
+
if (debug) {
|
159
|
+
_opts.backgroundColor = Math.random() * 0xffffff;
|
160
|
+
_opts.backgroundOpacity = 0.1;
|
151
161
|
}
|
152
|
-
|
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
|
-
|
185
|
+
|
172
186
|
const textOpts = {
|
173
|
-
|
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
|
-
|
190
|
-
|
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
|
205
|
-
//@ts-ignore
|
206
|
-
const opts: ThreeMeshUI.BlockOptions = {};
|
220
|
+
private getAlignment(opts: ThreeMeshUIEveryOptions): ThreeMeshUIEveryOptions {
|
207
221
|
|
208
|
-
|
209
|
-
opts.hiddenOverflow = hideOverflow;
|
222
|
+
opts.flexDirection = "column";
|
210
223
|
|
211
|
-
|
212
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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.
|
249
|
+
opts.alignItems = "start";
|
261
250
|
break;
|
262
251
|
case TextAnchor.MiddleLeft:
|
263
252
|
case TextAnchor.MiddleCenter:
|
264
253
|
case TextAnchor.MiddleRight:
|
265
|
-
opts.
|
254
|
+
opts.alignItems = "center";
|
266
255
|
break;
|
267
256
|
case TextAnchor.LowerLeft:
|
268
257
|
case TextAnchor.LowerCenter:
|
269
258
|
case TextAnchor.LowerRight:
|
270
|
-
opts.
|
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
|
297
|
-
if (
|
298
|
-
|
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
|
-
|
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
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
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
|
-
|
303
|
+
|
304
|
+
opts.textContent = this.getText(text, currentTag, next);
|
305
|
+
|
359
306
|
this.handleTag(currentTag, opts, stackArray);
|
360
|
-
this.
|
361
|
-
|
362
|
-
else {
|
363
|
-
|
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.
|
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
|
-
|
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 = {
|
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, {
|
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(
|
415
|
-
opts.
|
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.
|
376
|
+
opts.color = new Color(1, 1, 1);
|
420
377
|
}
|
421
|
-
}
|
422
|
-
|
378
|
+
} else if (tag.type == "b") {
|
379
|
+
this.setFont(opts, FontStyle.Bold);
|
423
380
|
const stackEntry = new TagStackEntry(tag, {
|
424
|
-
|
425
|
-
fontTexture: opts.fontTexture,
|
381
|
+
fontWeight: 700,
|
426
382
|
});
|
427
383
|
stackArray.push(stackEntry);
|
428
|
-
|
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
|
-
|
433
|
-
fontTexture: opts.fontTexture,
|
387
|
+
fontStyle: 'italic'
|
434
388
|
});
|
435
389
|
stackArray.push(stackEntry);
|
436
|
-
|
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
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
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
|
-
|
474
|
-
|
475
|
-
|
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
|
-
|
479
|
-
|
480
|
-
|
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
|
-
//
|
483
|
-
|
484
|
-
|
485
|
-
|
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 (
|
447
|
+
switch (fontStyle) {
|
448
|
+
default:
|
505
449
|
case FontStyle.Normal:
|
506
|
-
|
507
|
-
|
450
|
+
opts.fontWeight = 400;
|
451
|
+
opts.fontStyle = "normal";
|
452
|
+
break
|
453
|
+
|
508
454
|
case FontStyle.Bold:
|
509
|
-
|
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
|
-
|
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
|
-
|
516
|
-
|
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
|
-
|
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 {
|
@@ -9,36 +9,50 @@
|
|
9
9
|
export class TransformGizmo extends Behaviour {
|
10
10
|
|
11
11
|
@serializable()
|
12
|
-
public isGizmo: boolean =
|
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
|
-
|
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
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
this.
|
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
|
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
|
-
|
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
|
-
|
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
|
}
|
@@ -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 {
|
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") {
|
@@ -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
|
+
}
|
@@ -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
|
+
}
|