@@ -1,4 +1,4 @@
|
|
1
|
-
import fs from 'fs';
|
1
|
+
import fs, { statSync } from 'fs';
|
2
2
|
import crypto from 'crypto';
|
3
3
|
|
4
4
|
|
@@ -26,7 +26,7 @@
|
|
26
26
|
|
27
27
|
/** Recursively collect all files in a directory
|
28
28
|
* @param {String} directory to search
|
29
|
-
* @param {{ files: Array<{path:string, hash:string}>, totalsize:number }} info
|
29
|
+
* @param {{ files: Array<{path:string, hash:string, size:number}>, totalsize:number }} info
|
30
30
|
*/
|
31
31
|
function recursivelyCollectFiles(directory, path, info) {
|
32
32
|
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
@@ -44,13 +44,16 @@
|
|
44
44
|
} else {
|
45
45
|
const relpath = `${path}/${entry.name}`;
|
46
46
|
const filehash = crypto.createHash('sha256');
|
47
|
-
|
47
|
+
const fullpath = `${directory}/${entry.name}`;
|
48
|
+
filehash.update(fs.readFileSync(fullpath));
|
49
|
+
const stats = statSync(fullpath);
|
48
50
|
info.files.push({
|
49
51
|
path: relpath,
|
50
|
-
hash: filehash.digest('hex')
|
52
|
+
hash: filehash.digest('hex'),
|
53
|
+
size: stats.size
|
51
54
|
});
|
52
55
|
try {
|
53
|
-
const fullpath = `${directory}
|
56
|
+
const fullpath = `${ directory } / ${ entry.name }`;
|
54
57
|
const stats = fs.statSync(fullpath);
|
55
58
|
info.totalsize += stats.size;
|
56
59
|
}
|
@@ -41,7 +41,12 @@
|
|
41
41
|
cameraObject.position.y = 1;
|
42
42
|
cameraObject.position.z = 2;
|
43
43
|
|
44
|
-
|
44
|
+
const engineElement = evt.context.domElement as NeedleEngineHTMLElement
|
45
|
+
// If the camera is missing and the <needle-engine controls> is not set to false, create default camera controls
|
46
|
+
// That way we still create controls if the attribute is not added to <needle-engine> at all
|
47
|
+
if (engineElement?.cameraControls != false) {
|
48
|
+
createDefaultCameraControls(evt.context, cam);
|
49
|
+
}
|
45
50
|
|
46
51
|
return cam;
|
47
52
|
});
|
@@ -55,11 +60,11 @@
|
|
55
60
|
// check if the <needle-engine controls> is not set to false
|
56
61
|
const engineElement = evt.context.domElement as NeedleEngineHTMLElement
|
57
62
|
|
58
|
-
if (engineElement?.cameraControls
|
63
|
+
if (engineElement?.cameraControls != false) {
|
59
64
|
|
60
65
|
// Check if something else already acts as a camera controller
|
61
66
|
const existing = getCameraController(evt.context.mainCamera);
|
62
|
-
if (existing?.isCameraController
|
67
|
+
if (existing?.isCameraController == true) {
|
63
68
|
if (debug) console.log("Will not auto-fit because a camera controller exists");
|
64
69
|
return;
|
65
70
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { BoxGeometry, Material, Mesh, MeshStandardMaterial, PlaneGeometry, SphereGeometry } from "three"
|
1
|
+
import { BoxGeometry, Material, Mesh, MeshStandardMaterial, Object3D, PlaneGeometry, SphereGeometry, Sprite, SpriteMaterial, Texture } from "three"
|
2
2
|
|
3
3
|
import type { Vec3 } from "./engine_types.js";
|
4
4
|
|
@@ -9,9 +9,15 @@
|
|
9
9
|
}
|
10
10
|
export type PrimitiveTypeNames = keyof typeof PrimitiveType;
|
11
11
|
|
12
|
+
/**
|
13
|
+
* Options to create an object. Used by {@link ObjectUtils.createPrimitive}
|
14
|
+
*/
|
12
15
|
export type ObjectOptions = {
|
13
16
|
name?: string,
|
17
|
+
/** Optional: The material to apply to the object */
|
14
18
|
material?: Material,
|
19
|
+
/** Optional: The texture will applied to the material's main texture slot e.g. `material.map` if any is passed in */
|
20
|
+
texture?: Texture,
|
15
21
|
position?: Vec3,
|
16
22
|
/** euler */
|
17
23
|
rotation?: Vec3,
|
@@ -27,30 +33,62 @@
|
|
27
33
|
*/
|
28
34
|
export class ObjectUtils {
|
29
35
|
|
30
|
-
/** Creates a primitive object like a Cube or Sphere
|
36
|
+
/** Creates a primitive object like a Cube or Sphere
|
37
|
+
* @param type The type of primitive to create
|
38
|
+
* @param opts Options to create the object
|
39
|
+
* @returns The created object
|
40
|
+
*/
|
31
41
|
static createPrimitive(type: PrimitiveType | PrimitiveTypeNames, opts?: ObjectOptions): Mesh {
|
32
42
|
let obj: Mesh;
|
33
43
|
const color = 0xffffff;
|
34
44
|
switch (type) {
|
35
45
|
case "Quad":
|
36
46
|
case PrimitiveType.Quad:
|
37
|
-
|
38
|
-
|
39
|
-
|
47
|
+
{
|
48
|
+
const quadGeo = new PlaneGeometry(1, 1, 1, 1);
|
49
|
+
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
|
50
|
+
if (opts?.texture && "map" in mat) mat.map = opts.texture;
|
51
|
+
obj = new Mesh(quadGeo, mat);
|
52
|
+
}
|
40
53
|
break;
|
41
54
|
case "Cube":
|
42
55
|
case PrimitiveType.Cube:
|
43
|
-
|
44
|
-
|
45
|
-
|
56
|
+
{
|
57
|
+
const boxGeo = new BoxGeometry(1, 1, 1);
|
58
|
+
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
|
59
|
+
if (opts?.texture && "map" in mat) mat.map = opts.texture;
|
60
|
+
obj = new Mesh(boxGeo, mat);
|
61
|
+
}
|
46
62
|
break;
|
47
63
|
case "Sphere":
|
48
64
|
case PrimitiveType.Sphere:
|
49
|
-
|
50
|
-
|
51
|
-
|
65
|
+
{
|
66
|
+
const sphereGeo = new SphereGeometry(.5, 16, 16);
|
67
|
+
const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
|
68
|
+
if (opts?.texture && "map" in mat) mat.map = opts.texture;
|
69
|
+
obj = new Mesh(sphereGeo, mat);
|
70
|
+
}
|
52
71
|
break;
|
53
72
|
}
|
73
|
+
this.applyDefaultObjectOptions(obj, opts);
|
74
|
+
return obj;
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* Creates a Sprite object
|
79
|
+
* @param opts Options to create the object
|
80
|
+
* @returns The created object
|
81
|
+
*/
|
82
|
+
static createSprite(opts?: Omit<ObjectOptions, "material">): Sprite {
|
83
|
+
const color = 0xffffff;
|
84
|
+
const mat = new SpriteMaterial({ color: color });
|
85
|
+
if (opts?.texture && "map" in mat) mat.map = opts.texture;
|
86
|
+
const sprite = new Sprite(mat);
|
87
|
+
this.applyDefaultObjectOptions(sprite, opts);
|
88
|
+
return sprite;
|
89
|
+
}
|
90
|
+
|
91
|
+
private static applyDefaultObjectOptions(obj: Object3D, opts?: ObjectOptions) {
|
54
92
|
if (opts?.name)
|
55
93
|
obj.name = opts.name;
|
56
94
|
if (opts?.position)
|
@@ -59,6 +97,5 @@
|
|
59
97
|
obj.rotation.set(opts.rotation.x, opts.rotation.y, opts.rotation.z);
|
60
98
|
if (opts?.scale)
|
61
99
|
obj.scale.set(opts.scale.x, opts.scale.y, opts.scale.z);
|
62
|
-
return obj;
|
63
100
|
}
|
64
101
|
}
|
@@ -51,8 +51,23 @@
|
|
51
51
|
public get loadingProgress01(): number { return this._loadingProgress01; }
|
52
52
|
public get loadingFinished(): boolean { return this.loadingProgress01 > .999; }
|
53
53
|
|
54
|
-
|
54
|
+
/**
|
55
|
+
* If set to false the camera controls are disabled. Default is true.
|
56
|
+
* @type {boolean | null}
|
57
|
+
* @memberof NeedleEngineAttributes
|
58
|
+
* @example
|
59
|
+
* <needle-engine camera-controls="false"></needle-engine>
|
60
|
+
* @example
|
61
|
+
* <needle-engine camera-controls="true"></needle-engine>
|
62
|
+
* @example
|
63
|
+
* <needle-engine camera-controls></needle-engine>
|
64
|
+
* @example
|
65
|
+
* <needle-engine></needle-engine>
|
66
|
+
* @returns {boolean | null} if the attribute is not set it returns null
|
67
|
+
*/
|
68
|
+
public get cameraControls(): boolean | null {
|
55
69
|
const attr = this.getAttribute("camera-controls");
|
70
|
+
if (attr == null) return null;
|
56
71
|
if (attr === null || attr === "False" || attr === "false" || attr === "0") return false;
|
57
72
|
return true;
|
58
73
|
}
|
@@ -1,54 +1,90 @@
|
|
1
1
|
import { FrameEvent } from "./engine_context.js";
|
2
2
|
import { ContextEvent } from "./engine_context_registry.js";
|
3
|
-
import { type LifecycleMethod, registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
|
3
|
+
import { type LifecycleMethod, registerFrameEventCallback, unregisterFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
|
4
4
|
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Register a callback in the engine context created event.
|
8
8
|
* This happens once per context (after the context has been created and the first content has been loaded)
|
9
|
+
* @param cb The callback to be called
|
10
|
+
* @returns A function that can be called to unregister the callback
|
11
|
+
* @example
|
9
12
|
* ```ts
|
10
13
|
* onInitialized((ctx : Context) => {
|
11
14
|
* // do something
|
12
15
|
* }
|
13
16
|
* ```
|
14
17
|
* */
|
15
|
-
export function onInitialized(cb: LifecycleMethod) {
|
18
|
+
export function onInitialized(cb: LifecycleMethod): () => void {
|
16
19
|
registerFrameEventCallback(cb, ContextEvent.ContextCreated);
|
20
|
+
return () => unregisterFrameEventCallback(cb, ContextEvent.ContextCreated);
|
17
21
|
}
|
18
22
|
|
19
23
|
/** Register a callback in the engine start event.
|
20
24
|
* This happens at the beginning of each frame
|
25
|
+
* @param cb The callback to be called
|
26
|
+
* @returns A function that can be called to unregister the callback
|
27
|
+
* @example
|
21
28
|
* ```ts
|
22
29
|
* onStart((ctx : Context) => {
|
23
30
|
* // do something
|
24
31
|
* }
|
25
32
|
* ```
|
26
33
|
* */
|
27
|
-
export function onStart(cb: LifecycleMethod) {
|
34
|
+
export function onStart(cb: LifecycleMethod): () => void {
|
28
35
|
registerFrameEventCallback(cb, FrameEvent.Start);
|
36
|
+
return () => unregisterFrameEventCallback(cb, FrameEvent.Start);
|
29
37
|
}
|
30
38
|
|
31
39
|
|
32
40
|
/** Register a callback in the engine update event
|
33
41
|
* This is called every frame
|
42
|
+
* @param cb The callback to be called
|
43
|
+
* @returns A function that can be called to unregister the callback
|
44
|
+
* @example
|
34
45
|
* ```ts
|
35
46
|
* onUpdate((ctx : Context) => {
|
36
47
|
* // do something
|
37
48
|
* }
|
38
49
|
* ```
|
39
50
|
* */
|
40
|
-
export function onUpdate(cb: LifecycleMethod) {
|
51
|
+
export function onUpdate(cb: LifecycleMethod): () => void {
|
41
52
|
registerFrameEventCallback(cb, FrameEvent.Update);
|
53
|
+
return () => unregisterFrameEventCallback(cb, FrameEvent.Update);
|
42
54
|
}
|
43
55
|
|
44
56
|
/** Register a callback in the engine onBeforeRender event
|
45
|
-
* This is called every frame
|
57
|
+
* This is called every frame before the main camera renders
|
58
|
+
* @param cb The callback to be called
|
59
|
+
* @returns A function that can be called to unregister the callback
|
60
|
+
* @example
|
46
61
|
* ```ts
|
47
62
|
* onBeforeRender((ctx : Context) => {
|
48
63
|
* // do something
|
49
64
|
* }
|
50
65
|
* ```
|
51
66
|
* */
|
52
|
-
export function onBeforeRender(cb: LifecycleMethod) {
|
67
|
+
export function onBeforeRender(cb: LifecycleMethod): () => void {
|
53
68
|
registerFrameEventCallback(cb, FrameEvent.OnBeforeRender);
|
69
|
+
return () => unregisterFrameEventCallback(cb, FrameEvent.OnBeforeRender);
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Register a callback in the engine onAfterRender event
|
74
|
+
* This is called every frame after the main camera has rendered
|
75
|
+
* @param cb The callback to be called
|
76
|
+
* @returns A function that can be called to unregister the callback
|
77
|
+
* @example
|
78
|
+
* ```ts
|
79
|
+
* const unsubscribe = onAfterRender((ctx : Context) => {
|
80
|
+
* // do something...
|
81
|
+
* console.log("After render");
|
82
|
+
* // if you want to unsubscribe after the first call:
|
83
|
+
* unsubscribe();
|
84
|
+
* });
|
85
|
+
* ```
|
86
|
+
*/
|
87
|
+
export function onAfterRender(cb: LifecycleMethod): () => void {
|
88
|
+
registerFrameEventCallback(cb, FrameEvent.OnAfterRender);
|
89
|
+
return () => unregisterFrameEventCallback(cb, FrameEvent.OnAfterRender);
|
54
90
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -219,7 +219,7 @@
|
|
219
219
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
220
220
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
221
221
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
222
|
-
|
222
|
+
|
223
223
|
// Register types
|
224
224
|
TypeStore.add("__Ignore", __Ignore);
|
225
225
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -4,14 +4,23 @@
|
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
5
|
import { Behaviour, GameObject } from "./Component.js";
|
6
6
|
import { RGBAColor } from "./js-extensions/RGBAColor.js";
|
7
|
-
import { Renderer } from "./Renderer.js";
|
8
7
|
|
8
|
+
/**
|
9
|
+
* The mode of the ShadowCatcher.
|
10
|
+
* - ShadowMask: only renders shadows.
|
11
|
+
* - Additive: renders shadows additively.
|
12
|
+
* - Occluder: occludes light.
|
13
|
+
*/
|
9
14
|
enum ShadowMode {
|
10
15
|
ShadowMask = 0,
|
11
16
|
Additive = 1,
|
12
17
|
Occluder = 2,
|
13
18
|
}
|
14
19
|
|
20
|
+
/**
|
21
|
+
* ShadowCatcher can be added to an Object3D to make it render shadows (or light) in the scene. It can also be used to create a shadow mask, or to occlude light.
|
22
|
+
* If the GameObject is a Mesh, it will apply a shadow-catching material to it - otherwise it will create a quad with the shadow-catching material.
|
23
|
+
*/
|
15
24
|
export class ShadowCatcher extends Behaviour {
|
16
25
|
|
17
26
|
//@type Needle.Engine.ShadowCatcher.Mode
|
@@ -24,6 +33,7 @@
|
|
24
33
|
|
25
34
|
private targetMesh?: Mesh;
|
26
35
|
|
36
|
+
/** @internal */
|
27
37
|
start() {
|
28
38
|
// if there's no geometry, make a basic quad
|
29
39
|
if (!(this.gameObject instanceof Mesh)) {
|
@@ -49,7 +49,7 @@
|
|
49
49
|
// if the user is loading a GLB without a camera then the CameraUtils (which creates the default camera)
|
50
50
|
// checks if we have this attribute set and then sets the skybox clearflags accordingly
|
51
51
|
// if the user has a GLB with a camera but set to solid color then the skybox image is not visible -> we will just warn then and not override the camera settings
|
52
|
-
if(context.mainCameraComponent?.clearFlags !== ClearFlags.Skybox) console.warn("\"skybox-image\" attribute has no effect: camera clearflags are not set to \"Skybox\"");
|
52
|
+
if (context.mainCameraComponent?.clearFlags !== ClearFlags.Skybox) console.warn("\"skybox-image\" attribute has no effect: camera clearflags are not set to \"Skybox\"");
|
53
53
|
const promise = createRemoteSkyboxComponent(context, skyboxImage, true, false, "skybox-image");
|
54
54
|
promises.push(promise);
|
55
55
|
}
|
@@ -97,6 +97,32 @@
|
|
97
97
|
cache.push({ src, texture });
|
98
98
|
}
|
99
99
|
|
100
|
+
|
101
|
+
/**
|
102
|
+
* RemoteSkybox is a component that allows you to set the skybox of a scene from a URL or a local file.
|
103
|
+
* It supports .hdr, .exr, .jpg, .png files.
|
104
|
+
*
|
105
|
+
* ### Events
|
106
|
+
* - `dropped-unknown-url`: Emitted when a file is dropped on the scene. The event detail contains the sender, the url and a function to apply the url.
|
107
|
+
*
|
108
|
+
* @example adding a skybox
|
109
|
+
* ```ts
|
110
|
+
* GameObject.addComponent(gameObject, Skybox, { url: "https://example.com/skybox.hdr", background: true, environment: true });
|
111
|
+
* ```
|
112
|
+
*
|
113
|
+
* @example handle custom url
|
114
|
+
* ```ts
|
115
|
+
* const skybox = GameObject.addComponent(gameObject, Skybox);
|
116
|
+
* skybox.addEventListener("dropped-unknown-url", (evt) => {
|
117
|
+
* let url = evt.detail.url;
|
118
|
+
* console.log("User dropped file", url);
|
119
|
+
* // change url or resolve it differently
|
120
|
+
* url = "https://example.com/skybox.hdr";
|
121
|
+
* // apply the url
|
122
|
+
* evt.detail.apply(url);
|
123
|
+
* });
|
124
|
+
* ```
|
125
|
+
*/
|
100
126
|
export class RemoteSkybox extends Behaviour {
|
101
127
|
|
102
128
|
@syncField(RemoteSkybox.prototype.urlChangedSyncField)
|
@@ -113,6 +139,7 @@
|
|
113
139
|
environment: boolean = true;
|
114
140
|
|
115
141
|
/* set to false if you do not want to apply url change events via networking */
|
142
|
+
@serializable()
|
116
143
|
allowNetworking: boolean = true;
|
117
144
|
|
118
145
|
private _loader?: RGBELoader | EXRLoader | TextureLoader;
|
@@ -121,11 +148,13 @@
|
|
121
148
|
private _prevEnvironment: Texture | null = null;
|
122
149
|
private _prevBackground: any = null;
|
123
150
|
|
151
|
+
/** @internal */
|
124
152
|
onEnable() {
|
125
153
|
this.setSkybox(this.url);
|
126
154
|
this.registerDropEvents();
|
127
155
|
}
|
128
156
|
|
157
|
+
/** @internal */
|
129
158
|
onDisable() {
|
130
159
|
if (this.context.scene.environment === this._prevLoadedEnvironment) {
|
131
160
|
this.context.scene.environment = this._prevEnvironment;
|
@@ -139,15 +168,27 @@
|
|
139
168
|
}
|
140
169
|
|
141
170
|
private urlChangedSyncField() {
|
142
|
-
if (this.allowNetworking)
|
143
|
-
|
171
|
+
if (this.allowNetworking && this.url) {
|
172
|
+
// omit local dragged files from being handled
|
173
|
+
if (this.isRemoteTexture(this.url)) {
|
174
|
+
this.setSkybox(this.url);
|
175
|
+
}
|
176
|
+
}
|
144
177
|
}
|
145
178
|
|
146
|
-
|
179
|
+
/**
|
180
|
+
* Set the skybox from a given url
|
181
|
+
* @param url The url of the skybox image
|
182
|
+
* @param name Define name of the file with extension if it isn't apart of the url
|
183
|
+
* @returns Whether the skybox was successfully set
|
184
|
+
*/
|
185
|
+
async setSkybox(url: string | undefined | null, name?: string) {
|
147
186
|
if (!this.activeAndEnabled) return false;
|
148
187
|
if (!url) return false;
|
149
|
-
|
150
|
-
|
188
|
+
name ??= url;
|
189
|
+
|
190
|
+
if (!this.isValidTextureType(name)) {
|
191
|
+
console.warn("Potentially invalid skybox url", name, "on", this.name);
|
151
192
|
}
|
152
193
|
|
153
194
|
if (debug) console.log("Remote skybox url?: " + url);
|
@@ -162,7 +203,7 @@
|
|
162
203
|
}
|
163
204
|
this._prevUrl = url;
|
164
205
|
|
165
|
-
const envMap = await this.loadTexture(url);
|
206
|
+
const envMap = await this.loadTexture(url, name);
|
166
207
|
if (!envMap) return false;
|
167
208
|
// Check if we're still enabled
|
168
209
|
if (!this.enabled) return false;
|
@@ -178,15 +219,16 @@
|
|
178
219
|
return true;
|
179
220
|
}
|
180
221
|
|
181
|
-
private async loadTexture(url: string) {
|
222
|
+
private async loadTexture(url: string, name?: string) {
|
182
223
|
if (!url) return Promise.resolve(null);
|
183
|
-
|
224
|
+
name ??= url;
|
225
|
+
const cached = tryGetPreviouslyLoadedTexture(name);
|
184
226
|
if (cached) {
|
185
227
|
const res = await cached;
|
186
228
|
if (res.source?.data?.length > 0 || res.source?.data?.data?.length) return res;
|
187
229
|
}
|
188
|
-
const isEXR =
|
189
|
-
const isHdr =
|
230
|
+
const isEXR = name.endsWith(".exr");
|
231
|
+
const isHdr = name.endsWith(".hdr");
|
190
232
|
if (isEXR) {
|
191
233
|
if (!(this._loader instanceof EXRLoader))
|
192
234
|
this._loader = new EXRLoader();
|
@@ -202,7 +244,7 @@
|
|
202
244
|
|
203
245
|
if (debug) console.log("Loading skybox: " + url);
|
204
246
|
const loadingTask = this._loader.loadAsync(url);
|
205
|
-
registerLoadedTexture(
|
247
|
+
registerLoadedTexture(name, loadingTask);
|
206
248
|
const envMap = await loadingTask;
|
207
249
|
return envMap;
|
208
250
|
}
|
@@ -229,55 +271,110 @@
|
|
229
271
|
}
|
230
272
|
|
231
273
|
|
232
|
-
private
|
233
|
-
private dropEvent?: any;
|
274
|
+
private readonly validTextureTypes = [".hdr", ".exr", ".jpg", ".jpeg", ".png"];
|
234
275
|
|
276
|
+
private isRemoteTexture(url: string): boolean {
|
277
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
278
|
+
}
|
279
|
+
|
280
|
+
private isValidTextureType(url: string): boolean {
|
281
|
+
for (const type of this.validTextureTypes) {
|
282
|
+
if (url.endsWith(type)) return true;
|
283
|
+
}
|
284
|
+
return false;
|
285
|
+
}
|
286
|
+
|
287
|
+
|
288
|
+
|
235
289
|
private registerDropEvents() {
|
236
|
-
|
290
|
+
this.unregisterDropEvents();
|
291
|
+
this.context.domElement.addEventListener("dragover", this.onDragOverEvent);
|
292
|
+
this.context.domElement.addEventListener("drop", this.onDrop);
|
293
|
+
}
|
237
294
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
295
|
+
private unregisterDropEvents() {
|
296
|
+
this.context.domElement.removeEventListener("dragover", this.onDragOverEvent);
|
297
|
+
this.context.domElement.removeEventListener("drop", this.onDrop);
|
298
|
+
}
|
299
|
+
|
300
|
+
private onDragOverEvent = (e: DragEvent) => {
|
301
|
+
if (!this.allowDrop) return;
|
302
|
+
if (!e.dataTransfer) return;
|
303
|
+
for (const type of e.dataTransfer.types) {
|
304
|
+
// in ondragover we dont get access to the content
|
305
|
+
// but if we have a uri list we can assume
|
306
|
+
// someone is maybe dragging a image file
|
307
|
+
// so we want to capture this
|
308
|
+
if (type === "text/uri-list" || type === "Files") {
|
309
|
+
e.preventDefault();
|
310
|
+
}
|
311
|
+
}
|
312
|
+
};
|
313
|
+
|
314
|
+
private onDrop = (e: DragEvent) => {
|
315
|
+
if (!this.allowDrop) return;
|
316
|
+
if (!e.dataTransfer) return;
|
317
|
+
for (const type of e.dataTransfer.types) {
|
318
|
+
if (debug) console.log(type);
|
319
|
+
if (type === "text/uri-list") {
|
320
|
+
const url = e.dataTransfer.getData(type);
|
321
|
+
if (debug) console.log(type, url);
|
322
|
+
let name = new RegExp(/polyhaven.com\/asset_img\/.+?\/(?<name>.+)\.png/).exec(url)?.groups?.name;
|
323
|
+
if (!name) {
|
324
|
+
name = new RegExp(/polyhaven\.com\/a\/(?<name>.+)/).exec(url)?.groups?.name;
|
325
|
+
}
|
326
|
+
if (debug) console.log(name);
|
327
|
+
if (name) {
|
328
|
+
const skyboxurl = "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/" + name + "_1k.exr";
|
329
|
+
console.log(`[Remote Skybox] Setting skybox from url: ${skyboxurl}`);
|
247
330
|
e.preventDefault();
|
331
|
+
this.setSkybox(skyboxurl);
|
332
|
+
break;
|
248
333
|
}
|
249
|
-
|
250
|
-
|
334
|
+
else if (this.isValidTextureType(url)) {
|
335
|
+
console.log("[Remote Skybox] Setting skybox from url: " + url);
|
336
|
+
e.preventDefault();
|
337
|
+
this.setSkybox(url);
|
338
|
+
break;
|
339
|
+
}
|
340
|
+
else {
|
251
341
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
const envurl = "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/" + name + "_1k.exr";
|
267
|
-
this.setSkybox(envurl);
|
268
|
-
}
|
269
|
-
else console.warn("Could not resolve skybox name from dropped url", url);
|
342
|
+
console.warn(`[RemoteSkybox] Unknown url ${url}. If you want to load a skybox from a url, make sure it is a valid image url. Url must end with${this.validTextureTypes.join(", ")}.`);
|
343
|
+
// emit custom event - users can listen to this event and handle the url themselves
|
344
|
+
const evt = new CustomEvent("dropped-unknown-url", {
|
345
|
+
detail: {
|
346
|
+
sender: this,
|
347
|
+
event: e,
|
348
|
+
url,
|
349
|
+
apply: (url: string) => {
|
350
|
+
e.preventDefault();
|
351
|
+
this.setSkybox(url);
|
352
|
+
}
|
353
|
+
}
|
354
|
+
});
|
355
|
+
this.dispatchEvent(evt);
|
270
356
|
}
|
271
357
|
}
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
358
|
+
else if (type == "Files") {
|
359
|
+
const file = e.dataTransfer.files.item(0);
|
360
|
+
if (debug) console.log(type, file);
|
361
|
+
if (!file) continue;
|
362
|
+
if (!this.isValidTextureType(file.name)) {
|
363
|
+
console.warn(`[RemoteSkybox]: File \"${file.name}\" is not supported. Supported files are ${this.validTextureTypes.join(", ")}`);
|
364
|
+
return;
|
365
|
+
}
|
366
|
+
if (tryGetPreviouslyLoadedTexture(file.name) === null) {
|
367
|
+
const blob = new Blob([file]);
|
368
|
+
const url = URL.createObjectURL(blob);
|
369
|
+
e.preventDefault();
|
370
|
+
this.setSkybox(url, file.name);
|
371
|
+
}
|
372
|
+
else {
|
373
|
+
e.preventDefault();
|
374
|
+
this.setSkybox(file.name);
|
375
|
+
}
|
376
|
+
break;
|
377
|
+
}
|
378
|
+
}
|
379
|
+
};
|
283
380
|
}
|
@@ -6,24 +6,42 @@
|
|
6
6
|
import { getWorldPosition, getWorldQuaternion } from "../engine/engine_three_utils.js";
|
7
7
|
import { Behaviour } from "./Component.js";
|
8
8
|
|
9
|
+
/**
|
10
|
+
* SmoothFollow makes the {@link Object3D} (`GameObject`) smoothly follow another target {@link Object3D}.
|
11
|
+
* It can follow the target's position, rotation, or both.
|
12
|
+
*/
|
9
13
|
export class SmoothFollow extends Behaviour {
|
10
14
|
|
15
|
+
/**
|
16
|
+
* The target to follow. If null, the GameObject will not move.
|
17
|
+
*/
|
11
18
|
@serializable(Object3D)
|
12
19
|
target: Object3D | null = null;
|
13
20
|
|
21
|
+
/**
|
22
|
+
* The factor to smoothly follow the target's position.
|
23
|
+
* The value is clamped between 0 and 1. If 0, the GameObject will not follow the target's position.
|
24
|
+
*/
|
14
25
|
@serializable()
|
15
26
|
followFactor = .1;
|
27
|
+
/**
|
28
|
+
* The factor to smoothly follow the target's rotation.
|
29
|
+
* The value is clamped between 0 and 1. If 0, the GameObject will not follow the target's rotation.
|
30
|
+
*/
|
16
31
|
@serializable()
|
17
32
|
rotateFactor = .1;
|
18
33
|
|
19
34
|
@serializable()
|
20
|
-
positionAxes
|
35
|
+
positionAxes: Axes = Axes.All;
|
21
36
|
|
22
37
|
flipForward: boolean = false;
|
23
38
|
|
24
39
|
private static _invertForward: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
25
40
|
private _firstUpdate = true;
|
26
41
|
|
42
|
+
/**
|
43
|
+
* Update the position and rotation of the GameObject to follow the target.
|
44
|
+
*/
|
27
45
|
onBeforeRender(): void {
|
28
46
|
this.updateNow(false);
|
29
47
|
}
|
@@ -35,10 +53,10 @@
|
|
35
53
|
const fpos = this._firstUpdate || hard ? 1 : Mathf.clamp01(this.context.time.deltaTime * this.followFactor);
|
36
54
|
const currentPosition = this.worldPosition;
|
37
55
|
|
38
|
-
if(this.positionAxes & Axes.X) currentPosition.x = Mathf.lerp(currentPosition.x, wp.x, fpos);
|
39
|
-
if(this.positionAxes & Axes.Y) currentPosition.y = Mathf.lerp(currentPosition.y, wp.y, fpos);
|
40
|
-
if(this.positionAxes & Axes.Z) currentPosition.z = Mathf.lerp(currentPosition.z, wp.z, fpos);
|
41
|
-
|
56
|
+
if (this.positionAxes & Axes.X) currentPosition.x = Mathf.lerp(currentPosition.x, wp.x, fpos);
|
57
|
+
if (this.positionAxes & Axes.Y) currentPosition.y = Mathf.lerp(currentPosition.y, wp.y, fpos);
|
58
|
+
if (this.positionAxes & Axes.Z) currentPosition.z = Mathf.lerp(currentPosition.z, wp.z, fpos);
|
59
|
+
|
42
60
|
// TODO lerp distance from target as well
|
43
61
|
|
44
62
|
this.worldPosition = currentPosition;
|
@@ -132,6 +132,9 @@
|
|
132
132
|
}
|
133
133
|
}
|
134
134
|
|
135
|
+
/**
|
136
|
+
* The sprite renderer renders a sprite on a GameObject using an assigned spritesheet ({@link SpriteData})
|
137
|
+
*/
|
135
138
|
export class SpriteRenderer extends Behaviour {
|
136
139
|
|
137
140
|
@serializable()
|