@@ -433,8 +433,8 @@
|
|
433
433
|
const clip = action.getClip();
|
434
434
|
action.time = Mathf.lerp(options.minMaxOffsetNormalized.x, options.minMaxOffsetNormalized.y, Math.random()) * clip.duration;
|
435
435
|
}
|
436
|
-
// If the animation is
|
437
|
-
else if(
|
436
|
+
// If the animation is at the end, reset the time
|
437
|
+
else if(action.time >= action.getClip().duration) {
|
438
438
|
action.time = 0;
|
439
439
|
}
|
440
440
|
|
@@ -210,7 +210,12 @@
|
|
210
210
|
window.addEventListener("resize", hideQRCode);
|
211
211
|
window.addEventListener("scroll", hideQRCode);
|
212
212
|
|
213
|
-
|
213
|
+
// if we're in fullscreen:
|
214
|
+
if (document.fullscreenElement) {
|
215
|
+
document.fullscreenElement.appendChild(qrCodeContainer);
|
216
|
+
}
|
217
|
+
else
|
218
|
+
document.body.appendChild(qrCodeContainer);
|
214
219
|
}
|
215
220
|
|
216
221
|
/** hides to QRCode overlay and unsubscribes from events */
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { BufferGeometry, Group, Mesh, Vector3 } from "three"
|
1
|
+
import { BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three"
|
2
2
|
|
3
3
|
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
@@ -10,6 +10,7 @@
|
|
10
10
|
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
|
11
11
|
import { Behaviour } from "./Component.js";
|
12
12
|
import { Rigidbody } from "./RigidBody.js";
|
13
|
+
import { addComponent } from "../engine/engine_components.js";
|
13
14
|
|
14
15
|
/**
|
15
16
|
* Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.
|
@@ -130,6 +131,21 @@
|
|
130
131
|
*/
|
131
132
|
export class BoxCollider extends Collider implements IBoxCollider {
|
132
133
|
|
134
|
+
static add(obj: Mesh, opts?: { rigidbody: boolean }) {
|
135
|
+
const collider = new BoxCollider();
|
136
|
+
|
137
|
+
if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
|
138
|
+
const bb = obj.geometry.boundingBox!;
|
139
|
+
collider.size = bb!.getSize(new Vector3()) || new Vector3(1, 1, 1);
|
140
|
+
collider.center = bb!.getCenter(new Vector3()) || new Vector3(0, 0, 0);
|
141
|
+
addComponent(obj, collider);
|
142
|
+
|
143
|
+
if (opts?.rigidbody === true) {
|
144
|
+
addComponent(obj, Rigidbody, { isKinematic: false });
|
145
|
+
}
|
146
|
+
return collider;
|
147
|
+
}
|
148
|
+
|
133
149
|
@validate()
|
134
150
|
@serializable(Vector3)
|
135
151
|
size: Vector3 = new Vector3(1, 1, 1);
|
@@ -1,11 +1,20 @@
|
|
1
|
-
import { BoxGeometry, DoubleSide, Material, Mesh, MeshStandardMaterial, Object3D, PlaneGeometry, SphereGeometry, Sprite, SpriteMaterial, Texture } from "three"
|
1
|
+
import { BoxGeometry, BufferGeometry, ColorRepresentation, DoubleSide, ExtrudeGeometry, Material, Mesh, MeshStandardMaterial, Object3D, PlaneGeometry, Shape, SphereGeometry, Sprite, SpriteMaterial, Texture } from "three"
|
2
|
+
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
|
2
3
|
|
3
4
|
import type { Vec3 } from "./engine_types.js";
|
5
|
+
import { Font, FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
|
4
6
|
|
5
7
|
export enum PrimitiveType {
|
8
|
+
/**
|
9
|
+
* A quad with a width and height of 1 facing the positive Z axis
|
10
|
+
*/
|
6
11
|
Quad = 0,
|
12
|
+
/**
|
13
|
+
* A cube with a width, height, and depth of 1
|
14
|
+
*/
|
7
15
|
Cube = 1,
|
8
16
|
Sphere = 2,
|
17
|
+
RoundedCube = 10,
|
9
18
|
}
|
10
19
|
export type PrimitiveTypeNames = keyof typeof PrimitiveType;
|
11
20
|
|
@@ -13,18 +22,99 @@
|
|
13
22
|
* Options to create an object. Used by {@link ObjectUtils.createPrimitive}
|
14
23
|
*/
|
15
24
|
export type ObjectOptions = {
|
25
|
+
/**
|
26
|
+
* The parent object to add the created object to
|
27
|
+
*/
|
28
|
+
parent?: Object3D,
|
29
|
+
/**
|
30
|
+
* The name of the object
|
31
|
+
*/
|
16
32
|
name?: string,
|
17
|
-
/**
|
33
|
+
/** The material to apply to the object */
|
18
34
|
material?: Material,
|
19
|
-
/**
|
35
|
+
/** The color of the object. This color will only be used if no material is provided */
|
36
|
+
color?: ColorRepresentation,
|
37
|
+
/** The texture will applied to the material's main texture slot e.g. `material.map` if any is passed in */
|
20
38
|
texture?: Texture,
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
39
|
+
/**
|
40
|
+
* The position of the object in local space
|
41
|
+
*/
|
42
|
+
position?: Vec3 | [number, number, number],
|
43
|
+
/** The rotation of the object in local space */
|
44
|
+
rotation?: Vec3 | [number, number, number],
|
45
|
+
/**
|
46
|
+
* The scale of the object in local space
|
47
|
+
*/
|
48
|
+
scale?: Vec3 | number,
|
49
|
+
/**
|
50
|
+
* If the object should receive shadows
|
51
|
+
* @default true
|
52
|
+
*/
|
53
|
+
receiveShadow?: boolean,
|
54
|
+
/**
|
55
|
+
* If the object should cast shadows
|
56
|
+
* @default true
|
57
|
+
*/
|
58
|
+
castShadow?: boolean,
|
25
59
|
}
|
26
60
|
|
27
61
|
/**
|
62
|
+
* Options to create a 3D text object. Used by {@link ObjectUtils.createText}
|
63
|
+
*/
|
64
|
+
export type TextOptions = Omit<ObjectOptions, "texture"> & {
|
65
|
+
/**
|
66
|
+
* Optional: The font to use for the text. If not provided, the default font will be used
|
67
|
+
*/
|
68
|
+
font?: Font,
|
69
|
+
/**
|
70
|
+
* If the font is not provided, the familyFamily can be used to load a font from the default list
|
71
|
+
*/
|
72
|
+
familyFamily?: "OpenSans" | "Helvetiker";// "Optimer" | "Gentilis" | "DroidSans"
|
73
|
+
/**
|
74
|
+
* Optional: The depth of the text.
|
75
|
+
* @default .1
|
76
|
+
*/
|
77
|
+
depth?: number;
|
78
|
+
/**
|
79
|
+
* Optional: If the text should have a bevel effect
|
80
|
+
* @default false
|
81
|
+
*/
|
82
|
+
bevel?: boolean;
|
83
|
+
/**
|
84
|
+
* Invoked when the font geometry is loaded
|
85
|
+
*/
|
86
|
+
onGeometry?: (obj: Mesh) => void;
|
87
|
+
}
|
88
|
+
|
89
|
+
const fontsDict = new Map<string, Font | Promise<Font>>();
|
90
|
+
function loadFont(family: string | null): Font | Promise<Font> {
|
91
|
+
let url: string = "";
|
92
|
+
switch (family) {
|
93
|
+
default:
|
94
|
+
case "OpenSans":
|
95
|
+
url = "https://cdn.needle.tools/static/fonts/facetype/Open Sans_Regular_ascii.json";
|
96
|
+
break;
|
97
|
+
case "Helvetiker":
|
98
|
+
url = "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/fonts/helvetiker_regular.typeface.json";
|
99
|
+
break;
|
100
|
+
}
|
101
|
+
if (fontsDict.has(url)) {
|
102
|
+
const res = fontsDict.get(url);
|
103
|
+
if (res) return res;
|
104
|
+
}
|
105
|
+
const loader = new FontLoader();
|
106
|
+
const promise = new Promise<Font>((resolve, reject) => {
|
107
|
+
loader.load(url, res => {
|
108
|
+
fontsDict.set(url, res);
|
109
|
+
resolve(res);
|
110
|
+
}, undefined, reject);
|
111
|
+
});
|
112
|
+
fontsDict.set(url, promise);
|
113
|
+
return promise;
|
114
|
+
}
|
115
|
+
|
116
|
+
|
117
|
+
/**
|
28
118
|
* Utility class to create primitive objects
|
29
119
|
* @example
|
30
120
|
* ```typescript
|
@@ -34,6 +124,51 @@
|
|
34
124
|
export class ObjectUtils {
|
35
125
|
|
36
126
|
/**
|
127
|
+
* Creates a 3D text object
|
128
|
+
* @param text The text to display
|
129
|
+
* @param opts Options to create the object
|
130
|
+
*/
|
131
|
+
static createText(text: string, opts?: TextOptions): Mesh {
|
132
|
+
|
133
|
+
let geometry: BufferGeometry | null = null;
|
134
|
+
let font: Font | Promise<Font> = opts?.font || loadFont(opts?.familyFamily || null);
|
135
|
+
|
136
|
+
if (font instanceof Font) {
|
137
|
+
geometry = this.#createTextGeometry(text, font, opts);
|
138
|
+
}
|
139
|
+
else if (geometry == null) {
|
140
|
+
geometry = new BufferGeometry();
|
141
|
+
}
|
142
|
+
const color = opts?.color || 0xffffff;
|
143
|
+
const mesh = new Mesh(geometry, opts?.material ?? new MeshStandardMaterial({ color: color }));
|
144
|
+
this.applyDefaultObjectOptions(mesh, opts);
|
145
|
+
if (font instanceof Promise) {
|
146
|
+
font.then(res => {
|
147
|
+
mesh.geometry = this.#createTextGeometry(text, res, opts);
|
148
|
+
if (opts?.onGeometry) opts.onGeometry(mesh);
|
149
|
+
});
|
150
|
+
}
|
151
|
+
else {
|
152
|
+
if (opts?.onGeometry) opts.onGeometry(mesh);
|
153
|
+
}
|
154
|
+
return mesh;
|
155
|
+
}
|
156
|
+
static #createTextGeometry(text: string, font: Font, opts?: TextOptions) {
|
157
|
+
const depth = opts?.depth || .1;
|
158
|
+
const geo = new TextGeometry(text, {
|
159
|
+
font,
|
160
|
+
size: 1,
|
161
|
+
depth: depth,
|
162
|
+
height: depth,
|
163
|
+
bevelEnabled: opts?.bevel || false,
|
164
|
+
bevelThickness: .01,
|
165
|
+
bevelOffset: .01,
|
166
|
+
bevelSize: .01,
|
167
|
+
});
|
168
|
+
return geo;
|
169
|
+
}
|
170
|
+
|
171
|
+
/**
|
37
172
|
* Creates an occluder object that only render depth but not color
|
38
173
|
* @param type The type of primitive to create
|
39
174
|
* @returns The created object
|
@@ -50,7 +185,7 @@
|
|
50
185
|
*/
|
51
186
|
static createPrimitive(type: PrimitiveType | PrimitiveTypeNames, opts?: ObjectOptions): Mesh {
|
52
187
|
let obj: Mesh;
|
53
|
-
const color = 0xffffff;
|
188
|
+
const color = opts?.color || 0xffffff;
|
54
189
|
switch (type) {
|
55
190
|
case "Quad":
|
56
191
|
case PrimitiveType.Quad:
|
@@ -70,6 +205,15 @@
|
|
70
205
|
obj = new Mesh(boxGeo, mat);
|
71
206
|
}
|
72
207
|
break;
|
208
|
+
case PrimitiveType.RoundedCube:
|
209
|
+
case "RoundedCube":
|
210
|
+
{
|
211
|
+
const boxGeo = createBoxWithRoundedEdges(1, 1, 1, .1, 2);
|
212
|
+
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
|
213
|
+
if (opts?.texture && "map" in mat) mat.map = opts.texture;
|
214
|
+
obj = new Mesh(boxGeo, mat);
|
215
|
+
}
|
216
|
+
break;
|
73
217
|
case "Sphere":
|
74
218
|
case PrimitiveType.Sphere:
|
75
219
|
{
|
@@ -99,13 +243,61 @@
|
|
99
243
|
}
|
100
244
|
|
101
245
|
private static applyDefaultObjectOptions(obj: Object3D, opts?: ObjectOptions) {
|
246
|
+
obj.receiveShadow = true;
|
247
|
+
obj.castShadow = true;
|
248
|
+
|
102
249
|
if (opts?.name)
|
103
250
|
obj.name = opts.name;
|
104
|
-
if (opts?.position)
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
251
|
+
if (opts?.position) {
|
252
|
+
if (Array.isArray(opts.position))
|
253
|
+
obj.position.set(opts.position[0], opts.position[1], opts.position[2]);
|
254
|
+
else
|
255
|
+
obj.position.set(opts.position.x, opts.position.y, opts.position.z);
|
256
|
+
}
|
257
|
+
if (opts?.rotation) {
|
258
|
+
if (Array.isArray(opts.rotation))
|
259
|
+
obj.rotation.set(opts.rotation[0], opts.rotation[1], opts.rotation[2]);
|
260
|
+
else
|
261
|
+
obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
|
262
|
+
}
|
263
|
+
if (opts?.scale) {
|
264
|
+
if (typeof opts.scale === "number")
|
265
|
+
obj.scale.set(opts.scale, opts.scale, opts.scale);
|
266
|
+
else
|
267
|
+
obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
|
268
|
+
}
|
269
|
+
|
270
|
+
if (opts?.receiveShadow != undefined) {
|
271
|
+
obj.receiveShadow = opts.receiveShadow;
|
272
|
+
}
|
273
|
+
if (opts?.castShadow != undefined) {
|
274
|
+
obj.castShadow = opts.castShadow;
|
275
|
+
}
|
276
|
+
|
277
|
+
if (opts?.parent) {
|
278
|
+
opts.parent.add(obj);
|
279
|
+
}
|
110
280
|
}
|
111
|
-
}
|
281
|
+
}
|
282
|
+
|
283
|
+
function createBoxWithRoundedEdges(width: number, height: number, _depth: number, radius0: number, smoothness: number) {
|
284
|
+
const shape = new Shape();
|
285
|
+
const eps = 0.00001;
|
286
|
+
const radius = radius0 - eps;
|
287
|
+
shape.absarc(eps, eps, eps, -Math.PI / 2, -Math.PI, true);
|
288
|
+
shape.absarc(eps, height - radius * 2, eps, Math.PI, Math.PI / 2, true);
|
289
|
+
shape.absarc(width - radius * 2, height - radius * 2, eps, Math.PI / 2, 0, true);
|
290
|
+
shape.absarc(width - radius * 2, eps, eps, 0, -Math.PI / 2, true);
|
291
|
+
const geometry = new ExtrudeGeometry(shape, {
|
292
|
+
bevelEnabled: true,
|
293
|
+
bevelSegments: smoothness * 2,
|
294
|
+
steps: 1,
|
295
|
+
bevelSize: radius,
|
296
|
+
bevelThickness: radius0,
|
297
|
+
curveSegments: smoothness,
|
298
|
+
});
|
299
|
+
geometry.scale(1, 1, 1 - radius0)
|
300
|
+
geometry.center();
|
301
|
+
geometry.computeVertexNormals();
|
302
|
+
return geometry;
|
303
|
+
}
|
@@ -1239,7 +1239,7 @@
|
|
1239
1239
|
if (center && collider.gameObject) {
|
1240
1240
|
if (center.x !== 0 || center.y !== 0 || center.z !== 0) {
|
1241
1241
|
// TODO: fix export of center in editor integrations so we dont have to flip here
|
1242
|
-
this._tempCenterPos.x =
|
1242
|
+
this._tempCenterPos.x = center.x;
|
1243
1243
|
this._tempCenterPos.y = center.y;
|
1244
1244
|
this._tempCenterPos.z = center.z;
|
1245
1245
|
getWorldScale(collider.gameObject, this._tempCenterVec);
|
@@ -139,6 +139,7 @@
|
|
139
139
|
guid: string;
|
140
140
|
}
|
141
141
|
|
142
|
+
// TODO: we might want to separate the IComponent and IBehaviour where the Behaviour is the one with custom methods and the component only has e.g. the gameobject reference
|
142
143
|
export interface IComponent extends IHasGuid {
|
143
144
|
get isComponent(): boolean;
|
144
145
|
|
@@ -63,6 +63,8 @@
|
|
63
63
|
// make sure we have a unique material to work with
|
64
64
|
this.gameObject.material = this.gameObject.material.clone();
|
65
65
|
this.targetMesh = this.gameObject;
|
66
|
+
// make sure the mesh can receive shadows
|
67
|
+
this.targetMesh.receiveShadow = true;
|
66
68
|
}
|
67
69
|
|
68
70
|
if(!this.targetMesh) {
|