@@ -13,36 +13,46 @@
|
|
13
13
|
*/
|
14
14
|
export const needlePWA = (command, config, userSettings) => {
|
15
15
|
// @ts-ignore // TODO correctly type the userSettings.pwaOptions object
|
16
|
-
/** @type {import("vite-plugin-pwa").VitePWAOptions} */
|
17
|
-
const pwaOptions = userSettings.
|
16
|
+
/** @type {import("vite-plugin-pwa").VitePWAOptions | false} */
|
17
|
+
const pwaOptions = userSettings.pwa;
|
18
18
|
|
19
19
|
/** The context contains files that are generated by the plugin and should be deleted after
|
20
20
|
* @type {import('../types').NeedlePWAProcessContext} */
|
21
21
|
const context = { generatedFiles: [] }
|
22
22
|
|
23
|
+
// early out, explicitly disabled
|
24
|
+
if (pwaOptions === false) return;
|
25
|
+
|
26
|
+
// no PWA options provided, but the vite-pwa plugin is enabled – inform that we can do better
|
23
27
|
if (!pwaOptions) {
|
24
|
-
|
25
|
-
|
28
|
+
return {
|
29
|
+
name: "needle-pwa",
|
30
|
+
apply: "build",
|
31
|
+
configResolved(config) {
|
32
|
+
if (findVitePWAPlugin(config)) {
|
33
|
+
errorThrow("It seems that you're trying to build a PWA using `vite-plugin-pwa`!\nNeedle can manage PWA settings for you – just pass the same `pwaOptions` to the `needlePlugin` and `VitePWA` plugins:\n\n import { VitePWA } from 'vite-plugin-pwa';\n ...\n needlePlugins(command, needleConfig, { pwa: pwaOptions }),\n VitePWA(pwaOptions),\n\nIf you want to manage PWA building yourself and skip this check, please pass `{ pwa: false }` to needlePlugins.");
|
34
|
+
}
|
35
|
+
},
|
36
|
+
}
|
26
37
|
}
|
27
38
|
|
28
39
|
// @ts-ignore // TODO need an extra type so we can add more options into the VitePWAOptions object
|
29
40
|
/** @type {number | undefined} */
|
30
41
|
const updateInterval = pwaOptions.updateInterval || undefined;
|
31
42
|
|
32
|
-
if ((command !== "build" && !pwaOptions?.devOptions
|
33
|
-
if (userSettings?.noPWA === true) return;
|
43
|
+
if ((command !== "build" && !pwaOptions?.devOptions?.enabled) || pwaOptions?.disable) return;
|
34
44
|
|
35
45
|
// The PWA files should not have gzip compression – this will mess up with the precache and the service worker.
|
36
46
|
// If gzip is wanted, the server should serve files with gzip compression on the fly.
|
37
47
|
if (config) config.gzip = false;
|
38
48
|
|
39
49
|
if (!pwaOptions.registerType) {
|
40
|
-
log("Set PWA registerType to autoUpdate. This will automatically update the service worker when the content changes. If you want to manually update the service worker set the registerType to manual. See https://vite-pwa-org.netlify.app/guide/#configuring-vite-plugin-pwa");
|
50
|
+
// log("Set PWA registerType to autoUpdate. This will automatically update the service worker when the content changes. If you want to manually update the service worker set the registerType to manual. See https://vite-pwa-org.netlify.app/guide/#configuring-vite-plugin-pwa");
|
41
51
|
pwaOptions.registerType = "autoUpdate";
|
42
52
|
}
|
43
53
|
if (!pwaOptions.outDir) {
|
44
54
|
const outDir = getOutputDirectory();
|
45
|
-
log("Set PWA outDir to " + outDir);
|
55
|
+
// log("Set PWA outDir to " + outDir);
|
46
56
|
pwaOptions.outDir = outDir;
|
47
57
|
}
|
48
58
|
|
@@ -77,17 +87,20 @@
|
|
77
87
|
name: 'needle-pwa',
|
78
88
|
apply: 'build',
|
79
89
|
enforce: "post",
|
80
|
-
|
90
|
+
configResolved(config) {
|
81
91
|
try {
|
82
|
-
|
92
|
+
const plugin = findVitePWAPlugin(config);
|
93
|
+
if (!plugin) {
|
94
|
+
errorThrow("It seems that you're trying to build a PWA!.\nRun `npm install vite-plugin-pwa`, and add VitePWA to the vite config:\n\n import { VitePWA } from 'vite-plugin-pwa';\n ...\n needlePlugins(command, needleConfig, { pwa: pwaOptions }),\n VitePWA(pwaOptions),\n\nIf you don't intend to build a PWA, pass `{ pwa: false }` to needlePlugins or remove the `pwa` entry.");
|
95
|
+
}
|
83
96
|
|
84
97
|
// check if the index header contains the webmanifest ALSO
|
85
|
-
// if yes then we want to log a
|
98
|
+
// if yes then we want to log a warning at the end
|
86
99
|
const indexPath = currentDir + "/index.html";
|
87
100
|
if (existsSync(indexPath)) {
|
88
101
|
const indexContent = readFileSync(indexPath, 'utf8');
|
89
102
|
if (indexContent.includes(".webmanifest")) {
|
90
|
-
|
103
|
+
errorThrow("index.html contains a reference to a webmanifest. This is currently not supported. Please remove the reference from the index.html, or pass `{ pwa: false }` to needlePlugins to manage PWA building yourself.");
|
91
104
|
}
|
92
105
|
}
|
93
106
|
}
|
@@ -189,28 +202,21 @@
|
|
189
202
|
|
190
203
|
/** Checks if the vite-plugin-pwa is present in the vite config
|
191
204
|
* @param {import('vite').UserConfig} config
|
205
|
+
* @returns {import('vite-plugin-pwa').VitePWAOptions | null}
|
192
206
|
*/
|
193
|
-
function
|
207
|
+
function findVitePWAPlugin(config) {
|
194
208
|
const plugins = config.plugins || [];
|
195
|
-
|
209
|
+
function _findVitePWAPlugin(p) {
|
210
|
+
if (Array.isArray(p))
|
211
|
+
return p.find(_findVitePWAPlugin);
|
212
|
+
if (p?.name === "vite-plugin-pwa")
|
213
|
+
return p;
|
214
|
+
}
|
196
215
|
for (const plugin of plugins) {
|
197
|
-
|
198
|
-
|
199
|
-
return;
|
200
|
-
}
|
216
|
+
const foundVitePWAPlugin = _findVitePWAPlugin(plugin);
|
217
|
+
if (foundVitePWAPlugin) return foundVitePWAPlugin;
|
201
218
|
}
|
202
|
-
|
203
|
-
if (Array.isArray(p)) {
|
204
|
-
return p.some(isVitePWAPlugin);
|
205
|
-
}
|
206
|
-
return p.name === "vite-plugin-pwa";
|
207
|
-
}
|
208
|
-
if (!foundVitePWAPlugin) {
|
209
|
-
// const { default: VitePWA } = await import('vite-plugin-pwa');
|
210
|
-
// log("Add vite-plugin-pwa to the vite config");
|
211
|
-
// config.plugins.push(VitePWA());
|
212
|
-
throw new Error("ERR: [needle-pwa] vite-plugin-pwa is not present in the vite config. Please install vite-plugin-pwa and add it to the vite config like so: \"VitePWA(PWAOptions)\". \n\nIf you don't intent to build a PWA then don't pass the PWA options to the needlePlugin or set `noPWA: true` in the user settings (third argument).");
|
213
|
-
}
|
219
|
+
return null;
|
214
220
|
}
|
215
221
|
|
216
222
|
function cleanup(context) {
|
@@ -225,6 +231,18 @@
|
|
225
231
|
console.log("[needle-pwa]", ...args);
|
226
232
|
}
|
227
233
|
|
234
|
+
/** Throws an error with defined stacktrace.
|
235
|
+
* @param {string} message
|
236
|
+
* @param {number} stackTraceLimit How many stack frames to show in the error message
|
237
|
+
*/
|
238
|
+
function errorThrow(message, traceLimit = 0) {
|
239
|
+
const { stackTraceLimit } = Error;
|
240
|
+
Error.stackTraceLimit = traceLimit;
|
241
|
+
const e = new Error("[needle-pwa] " + message);
|
242
|
+
Error.stackTraceLimit = stackTraceLimit;
|
243
|
+
throw e;
|
244
|
+
}
|
245
|
+
|
228
246
|
/**
|
229
247
|
* @param {string | Partial<import("vite-plugin-pwa").ManifestOptions>} webmanifestPath Path to the webmanifest file, or partial manifest itself
|
230
248
|
* @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
|
@@ -249,7 +267,11 @@
|
|
249
267
|
}
|
250
268
|
|
251
269
|
processIcons(manifest, outDir, context);
|
252
|
-
|
270
|
+
|
271
|
+
// TODO include assets in the manifest instead of manually copying
|
272
|
+
// if (pwaOptions.includeAssets === undefined) pwaOptions.includeAssets = [];
|
273
|
+
// pwaOptions.includeAssets = [...pwaOptions.includeAssets, ...manifest.icons?.map(i => i.src)];
|
274
|
+
|
253
275
|
const packageJsonPath = process.cwd() + "/package.json";
|
254
276
|
const packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : {};
|
255
277
|
|
@@ -347,31 +369,28 @@
|
|
347
369
|
function generateIcons(manifest, context) {
|
348
370
|
if (!manifest.icons) manifest.icons = [];
|
349
371
|
log("Generating PWA icons");
|
350
|
-
const sizes = [192, 512];
|
372
|
+
const sizes = [48, 128, 144, 192, 512];
|
373
|
+
// Needle icon
|
351
374
|
const defaultIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 160 187.74"><defs><linearGradient id="a" x1="89.64" y1="184.81" x2="90.48" y2="21.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62d399"/><stop offset=".51" stop-color="#acd842"/><stop offset=".9" stop-color="#d7db0a"/></linearGradient><linearGradient id="b" x1="69.68" y1="178.9" x2="68.08" y2="16.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ba398"/><stop offset=".5" stop-color="#4ca352"/><stop offset="1" stop-color="#76a30a"/></linearGradient><linearGradient id="c" x1="36.6" y1="152.17" x2="34.7" y2="84.19" gradientUnits="userSpaceOnUse"><stop offset=".19" stop-color="#36a382"/><stop offset=".54" stop-color="#49a459"/><stop offset="1" stop-color="#76a30b"/></linearGradient><linearGradient id="d" x1="15.82" y1="153.24" x2="18" y2="90.86" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#267880"/><stop offset=".51" stop-color="#457a5c"/><stop offset="1" stop-color="#717516"/></linearGradient><linearGradient id="e" x1="135.08" y1="135.43" x2="148.93" y2="63.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0d939"/><stop offset="1" stop-color="#eadb04"/></linearGradient><linearGradient id="f" x1="-4163.25" y1="2285.12" x2="-4160.81" y2="2215.34" gradientTransform="rotate(20 4088.49 13316.712)" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#74af52"/><stop offset=".48" stop-color="#99be32"/><stop offset="1" stop-color="#c0c40a"/></linearGradient><symbol id="g" viewBox="0 0 160 187.74"><path style="fill:url(#a)" d="M79.32 36.98v150.76L95 174.54l6.59-156.31-22.27 18.75z"/><path style="fill:url(#b)" d="M79.32 36.98 57.05 18.23l6.59 156.31 15.68 13.2V36.98z"/><path style="fill:url(#c)" d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z"/><path style="fill:url(#d)" d="M25.19 104.83 0 90.24l16.97 53.86 16.85 9.77-8.63-49.04z"/><path style="fill:#9c3" d="M43.86 82.5 18.69 67.98 0 90.24l25.18 14.59L43.86 82.5z"/><path style="fill:url(#e)" d="m134.82 78.69-9.97 56.5 15.58-9.04L160 64.1l-25.18 14.59z"/><path style="fill:url(#f)" d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z"/><path style="fill:#ffe113" d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33L160 64.1z"/><path style="fill:#f3e600" d="M101.59 18.23 79.32 0 57.05 18.23l22.27 18.75 22.27-18.75z"/></symbol></defs><use width="160" height="187.74" xlink:href="#g"/></svg>`;
|
352
375
|
const iconDir = process.cwd();
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
manifest.icons.push({
|
368
|
-
...iconInfo,
|
369
|
-
purpose: "maskable"
|
370
|
-
})
|
371
|
-
}
|
376
|
+
|
377
|
+
const iconName = "pwa-icon-allSizes.svg";
|
378
|
+
const iconPath = iconDir + "/" + iconName;
|
379
|
+
writeFileSync(iconPath, defaultIconSVG, 'utf8');
|
380
|
+
const iconSrc = "./" + iconName;
|
381
|
+
// log("Generated PWA icon", iconSrc);
|
382
|
+
const iconInfo = {
|
383
|
+
src: iconSrc,
|
384
|
+
type: "image/svg+xml",
|
385
|
+
sizes: sizes.map(s => s + "x" + s).join(" "),
|
386
|
+
"purpose": "any"
|
387
|
+
};
|
388
|
+
manifest.icons.push(iconInfo);
|
389
|
+
context.generatedFiles.push(iconPath);
|
372
390
|
}
|
373
391
|
|
374
392
|
/** Tries to copy the icons to the output directory
|
393
|
+
* TODO this should not be needed if we use pwaOptions.includeAssets
|
375
394
|
* @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
|
376
395
|
*/
|
377
396
|
function copyIcons(manifest, outDir) {
|
@@ -1,8 +1,12 @@
|
|
1
|
-
import { CustomBlending, DoubleSide, FrontSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, Object3D, OrthographicCamera, PlaneGeometry, RenderItem, ShaderMaterial, WebGLRenderTarget } from "three";
|
1
|
+
import { CustomBlending, DoubleSide, FrontSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, Object3D, OrthographicCamera, PlaneGeometry, RenderItem, ShaderMaterial, Vector3, WebGLRenderTarget } from "three";
|
2
2
|
import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
|
3
3
|
import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
|
4
4
|
|
5
|
+
import { addComponent } from "../engine/engine_components.js";
|
6
|
+
import { Context } from "../engine/engine_context.js";
|
7
|
+
import { Gizmos } from "../engine/engine_gizmos.js";
|
5
8
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
9
|
+
import { getBoundingBox } from "../engine/engine_three_utils.js";
|
6
10
|
import { getParam } from "../engine/engine_utils.js"
|
7
11
|
import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
|
8
12
|
import { Behaviour } from "./Component.js";
|
@@ -21,7 +25,33 @@
|
|
21
25
|
*/
|
22
26
|
export class ContactShadows extends Behaviour {
|
23
27
|
|
28
|
+
private static _instance: ContactShadows;
|
29
|
+
/**
|
30
|
+
* Create contact shadows for the scene. Automatically fits the shadows to the scene.
|
31
|
+
* The instance of contact shadows will be created only once.
|
32
|
+
* @param context The context to create the contact shadows in.
|
33
|
+
* @returns The instance of the contact shadows.
|
34
|
+
*/
|
35
|
+
static auto(context?: Context): ContactShadows {
|
36
|
+
if (!this._instance || this._instance.destroyed) {
|
37
|
+
const obj = new Object3D();
|
38
|
+
this._instance = addComponent(obj, ContactShadows, {
|
39
|
+
autoFit: false,
|
40
|
+
occludeBelowGround: false
|
41
|
+
});
|
42
|
+
}
|
43
|
+
if (!context) context = Context.Current;
|
44
|
+
context.scene.add(this._instance.gameObject);
|
45
|
+
this._instance.fitShadows();
|
46
|
+
return this._instance;
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* When enabled the contact shadows component will be created to fit the whole scene.
|
51
|
+
*/
|
24
52
|
@serializable()
|
53
|
+
autoFit: boolean = false;
|
54
|
+
@serializable()
|
25
55
|
darkness: number = 0.5;
|
26
56
|
@serializable()
|
27
57
|
opacity: number = 0.5;
|
@@ -46,9 +76,22 @@
|
|
46
76
|
private horizontalBlurMaterial?: ShaderMaterial;
|
47
77
|
private verticalBlurMaterial?: ShaderMaterial;
|
48
78
|
|
79
|
+
/**
|
80
|
+
* Call to fit the shadows to the scene.
|
81
|
+
*/
|
82
|
+
fitShadows() {
|
83
|
+
if (debug) console.log("Fitting shadows to scene");
|
84
|
+
const box = getBoundingBox(this.context.scene.children, [this.gameObject]);
|
85
|
+
if (debug) Gizmos.DrawWireBox3(box, 0xffff00, 1);
|
86
|
+
const min = box.min;
|
87
|
+
this.gameObject.position.set((min.x + box.max.x) / 2, min.y - .01, (min.z + box.max.z) / 2);
|
88
|
+
this.gameObject.scale.set(box.max.x - min.x, 1, box.max.z - min.z);
|
89
|
+
this.gameObject.scale.multiplyScalar(1.5);
|
90
|
+
}
|
91
|
+
|
49
92
|
/** @internal */
|
50
93
|
start(): void {
|
51
|
-
if(debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
|
94
|
+
if (debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
|
52
95
|
const textureSize = 512;
|
53
96
|
|
54
97
|
this.shadowGroup = new Group();
|
@@ -64,14 +107,16 @@
|
|
64
107
|
|
65
108
|
// make a plane and make it face up
|
66
109
|
const planeGeometry = new PlaneGeometry(1, 1).rotateX(Math.PI / 2);
|
67
|
-
|
110
|
+
|
68
111
|
if (this.gameObject instanceof Mesh) {
|
69
|
-
|
70
|
-
this.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
mat.
|
112
|
+
console.warn("ContactShadows can not be added to a Mesh. Please add it to a Group or an empty Object");
|
113
|
+
// this.enabled = false;
|
114
|
+
setCustomVisibility(this.gameObject, false);
|
115
|
+
// this.plane = this.gameObject as any as Mesh;
|
116
|
+
// // Make sure we clone the material once because it might be used on another object as well
|
117
|
+
// const mat = this.plane.material = (this.plane.material as MeshBasicMaterial).clone();
|
118
|
+
// mat.map = this.renderTarget.texture;
|
119
|
+
// mat.opacity = this.opacity;
|
75
120
|
// mat.transparent = true;
|
76
121
|
// mat.depthWrite = false;
|
77
122
|
// mat.needsUpdate = true;
|
@@ -80,33 +125,30 @@
|
|
80
125
|
// mat.transparent = true;
|
81
126
|
// mat.depthWrite = false;
|
82
127
|
}
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
128
|
+
|
129
|
+
const planeMaterial = new MeshBasicMaterial({
|
130
|
+
map: this.renderTarget.texture,
|
131
|
+
opacity: this.opacity,
|
132
|
+
color: 0x000000,
|
133
|
+
transparent: true,
|
134
|
+
depthWrite: false,
|
135
|
+
});
|
136
|
+
this.plane = new Mesh(planeGeometry, planeMaterial);
|
137
|
+
this.plane.scale.y = - 1;
|
138
|
+
this.gameObject.add(this.plane);
|
139
|
+
|
95
140
|
if (this.plane) this.plane.renderOrder = 1;
|
96
141
|
|
142
|
+
this.occluderMesh = new Mesh(this.plane.geometry, new MeshBasicMaterial({
|
143
|
+
depthWrite: true,
|
144
|
+
stencilWrite: true,
|
145
|
+
colorWrite: false,
|
146
|
+
}))
|
147
|
+
// .rotateX(Math.PI)
|
148
|
+
.translateY(-0.0001);
|
149
|
+
this.occluderMesh.renderOrder = -100;
|
150
|
+
this.gameObject.add(this.occluderMesh);
|
97
151
|
|
98
|
-
if (this.occludeBelowGround) {
|
99
|
-
this.occluderMesh = new Mesh(this.plane.geometry, new MeshBasicMaterial({
|
100
|
-
depthWrite: true,
|
101
|
-
stencilWrite: true,
|
102
|
-
colorWrite: false,
|
103
|
-
}))
|
104
|
-
// .rotateX(Math.PI)
|
105
|
-
.translateY(-0.0001);
|
106
|
-
this.occluderMesh.renderOrder = -100;
|
107
|
-
this.gameObject.add(this.occluderMesh);
|
108
|
-
}
|
109
|
-
|
110
152
|
// the plane onto which to blur the texture
|
111
153
|
this.blurPlane = new Mesh(planeGeometry);
|
112
154
|
this.blurPlane.visible = false;
|
@@ -123,8 +165,6 @@
|
|
123
165
|
// this will properly overlap calculated shadows
|
124
166
|
this.depthMaterial.blending = CustomBlending;
|
125
167
|
this.depthMaterial.blendEquation = MaxEquation;
|
126
|
-
if (this.backfaceShadows)
|
127
|
-
this.depthMaterial.side = DoubleSide;
|
128
168
|
|
129
169
|
// this.depthMaterial.blendEquation = MinEquation;
|
130
170
|
this.depthMaterial.onBeforeCompile = shader => {
|
@@ -150,6 +190,8 @@
|
|
150
190
|
this.verticalBlurMaterial.depthTest = false;
|
151
191
|
|
152
192
|
this.shadowGroup.visible = false;
|
193
|
+
|
194
|
+
if (this.autoFit) this.fitShadows();
|
153
195
|
}
|
154
196
|
|
155
197
|
/** @internal */
|
@@ -197,7 +239,7 @@
|
|
197
239
|
this.plane.visible = false;
|
198
240
|
|
199
241
|
if (this.gameObject instanceof Mesh) {
|
200
|
-
this.gameObject.visible = false;
|
242
|
+
// this.gameObject.visible = false;
|
201
243
|
setCustomVisibility(this.gameObject, false);
|
202
244
|
}
|
203
245
|
|
@@ -207,6 +249,11 @@
|
|
207
249
|
|
208
250
|
// force the depthMaterial to everything
|
209
251
|
scene.overrideMaterial = this.depthMaterial;
|
252
|
+
if (this.backfaceShadows)
|
253
|
+
this.depthMaterial.side = DoubleSide;
|
254
|
+
else {
|
255
|
+
this.depthMaterial.side = FrontSide;
|
256
|
+
}
|
210
257
|
|
211
258
|
// set renderer clear alpha
|
212
259
|
const initialClearAlpha = renderer.getClearAlpha();
|
@@ -240,7 +287,7 @@
|
|
240
287
|
this.blurShadow(this.blur * 0.4);
|
241
288
|
|
242
289
|
this.shadowGroup.visible = false;
|
243
|
-
if (this.occluderMesh) this.occluderMesh.visible =
|
290
|
+
if (this.occluderMesh) this.occluderMesh.visible = this.occludeBelowGround;
|
244
291
|
this.plane.visible = planeWasVisible;
|
245
292
|
|
246
293
|
// reset and render the normal scene
|
@@ -58,6 +58,7 @@
|
|
58
58
|
export declare class ContextCreateArgs {
|
59
59
|
/** list of glTF or GLB files to load */
|
60
60
|
files: Array<string>;
|
61
|
+
abortSignal?: AbortSignal;
|
61
62
|
/** called when loading a provided glTF file started */
|
62
63
|
onLoadingStart?: (index: number, file: string) => void;
|
63
64
|
/** called on update for each loaded glTF file */
|
@@ -821,7 +822,9 @@
|
|
821
822
|
prepare_succeeded = false;
|
822
823
|
}
|
823
824
|
if (!prepare_succeeded) return false;
|
824
|
-
if (createId !== this._createId)
|
825
|
+
if (createId !== this._createId || opts?.abortSignal?.aborted) {
|
826
|
+
return false;
|
827
|
+
}
|
825
828
|
|
826
829
|
this.internalOnUpdateVisible();
|
827
830
|
|
@@ -946,11 +949,14 @@
|
|
946
949
|
this.domElement?.internalSetLoadingMessage("finish loading");
|
947
950
|
await res;
|
948
951
|
}
|
952
|
+
if (opts?.abortSignal?.aborted) {
|
953
|
+
return false;
|
954
|
+
}
|
949
955
|
invokeLifecycleFunctions(this, ContextEvent.ContextCreated);
|
950
956
|
if (debug) console.log("Context Created...", this.renderer, this.renderer.domElement)
|
951
957
|
|
952
958
|
this._isCreating = false;
|
953
|
-
if (!this.isManagedExternally)
|
959
|
+
if (!this.isManagedExternally && !opts?.abortSignal?.aborted)
|
954
960
|
this.restartRenderLoop();
|
955
961
|
return res;
|
956
962
|
}
|
@@ -973,6 +979,10 @@
|
|
973
979
|
// this hash should be constant since it is used to initialize the UIDProvider per initially loaded scene
|
974
980
|
const loadingHash = 0;
|
975
981
|
for (let i = 0; i < files.length; i++) {
|
982
|
+
if (args.abortSignal?.aborted) {
|
983
|
+
if (debug) console.log("Aborting loading because of abort signal");
|
984
|
+
break;
|
985
|
+
}
|
976
986
|
// abort loading if the create id has changed
|
977
987
|
if (createId !== this._createId) {
|
978
988
|
if (debug) console.log("Aborting loading because create id changed", createId, this._createId);
|
@@ -982,6 +992,7 @@
|
|
982
992
|
args?.onLoadingStart?.call(this, i, file);
|
983
993
|
if (debug) console.log("Context Load " + file);
|
984
994
|
const res = await loader.loadSync(this, file, file, loadingHash, prog => {
|
995
|
+
if (args.abortSignal?.aborted) return;
|
985
996
|
progressArg.name = file;
|
986
997
|
progressArg.progress = prog;
|
987
998
|
progressArg.index = i;
|
@@ -1003,7 +1014,7 @@
|
|
1003
1014
|
|
1004
1015
|
// if the id was changed while still loading
|
1005
1016
|
// then we want to cleanup/destroy previously loaded files
|
1006
|
-
if (createId !== this._createId) {
|
1017
|
+
if (createId !== this._createId || args.abortSignal?.aborted) {
|
1007
1018
|
for (const res of results) {
|
1008
1019
|
if (res && res.file) {
|
1009
1020
|
for (const scene of res.file.scenes)
|
@@ -208,7 +208,8 @@
|
|
208
208
|
this._loadingElement.style.flexDirection = "column";
|
209
209
|
this._loadingElement.style.pointerEvents = "none";
|
210
210
|
this._loadingElement.style.color = "white";
|
211
|
-
this._loadingElement.style.fontFamily =
|
211
|
+
this._loadingElement.style.fontFamily = 'system-ui, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
|
212
|
+
this._loadingElement.style.fontSize = "1rem";
|
212
213
|
if (loadingStyle === "light")
|
213
214
|
this._loadingElement.style.color = "rgba(0,0,0,.6)";
|
214
215
|
else
|
@@ -332,9 +333,8 @@
|
|
332
333
|
const messageContainer = document.createElement("div");
|
333
334
|
this._messageContainer = messageContainer;
|
334
335
|
messageContainer.style.display = "flex";
|
335
|
-
messageContainer.style.fontSize = ".
|
336
|
+
messageContainer.style.fontSize = ".8rem";
|
336
337
|
messageContainer.style.paddingTop = ".2rem";
|
337
|
-
messageContainer.style.fontWeight = "200";
|
338
338
|
// messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
|
339
339
|
messageContainer.style.justifyContent = "center";
|
340
340
|
details.appendChild(messageContainer);
|
@@ -261,7 +261,7 @@
|
|
261
261
|
if (debug) console.log("attributeChangedCallback", name, _oldValue, newValue);
|
262
262
|
switch (name) {
|
263
263
|
case "src":
|
264
|
-
if (debug) console.warn("src
|
264
|
+
if (debug) console.warn("<needle-engine src>\nchanged from \"", _oldValue, "\" to \"", newValue, "\"")
|
265
265
|
this.onLoad();
|
266
266
|
// this._watcher?.onSourceChanged(newValue);
|
267
267
|
break;
|
@@ -297,7 +297,9 @@
|
|
297
297
|
}
|
298
298
|
|
299
299
|
private _loadId: number = 0;
|
300
|
+
private _abortController: AbortController | null = null;
|
300
301
|
private _lastSourceFiles: Array<string> | null = null;
|
302
|
+
private _createContextPromise: Promise<any> | null = null;
|
301
303
|
|
302
304
|
private async onLoad() {
|
303
305
|
|
@@ -313,16 +315,25 @@
|
|
313
315
|
}
|
314
316
|
|
315
317
|
const filesToLoad = this.getSourceFiles();
|
318
|
+
if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
|
319
|
+
return;
|
320
|
+
}
|
321
|
+
|
322
|
+
// Abort previous loading (if it's still running)
|
323
|
+
if (this._abortController) {
|
324
|
+
if (debug) console.warn("Abort previous loading process")
|
325
|
+
this._abortController.abort();
|
326
|
+
this._abortController = null;
|
327
|
+
}
|
328
|
+
this._lastSourceFiles = filesToLoad;
|
329
|
+
const loadId = ++this._loadId;
|
330
|
+
|
316
331
|
if (filesToLoad === null || filesToLoad === undefined || filesToLoad.length <= 0) {
|
317
332
|
if (debug) console.warn("Clear scene", filesToLoad);
|
318
333
|
this._context.clear();
|
334
|
+
if (loadId !== this._loadId) return;
|
319
335
|
}
|
320
|
-
else if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
|
321
|
-
return;
|
322
|
-
}
|
323
336
|
|
324
|
-
this._lastSourceFiles = filesToLoad;
|
325
|
-
const loadId = ++this._loadId;
|
326
337
|
const alias = this.getAttribute("alias");
|
327
338
|
this.classList.add("loading");
|
328
339
|
|
@@ -371,7 +382,7 @@
|
|
371
382
|
}, 300)
|
372
383
|
}
|
373
384
|
}
|
374
|
-
if (debug) console.warn("
|
385
|
+
if (debug) console.warn("--------------\nNeedle Engine: Begin loading " + loadId + "\n", filesToLoad);
|
375
386
|
this.onBeforeBeginLoading();
|
376
387
|
|
377
388
|
const loadedFiles: Array<LoadedGLTF> = [];
|
@@ -385,9 +396,13 @@
|
|
385
396
|
};
|
386
397
|
const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
|
387
398
|
const displayNames = new Array<string>();
|
399
|
+
const controller = new AbortController();
|
400
|
+
this._abortController = controller;
|
388
401
|
const args: ContextCreateArgs = {
|
389
402
|
files: filesToLoad,
|
403
|
+
abortSignal: controller.signal,
|
390
404
|
onLoadingProgress: evt => {
|
405
|
+
if (controller.signal.aborted) return;
|
391
406
|
const index = evt.index;
|
392
407
|
if (!displayNames[index] && evt.name) {
|
393
408
|
displayNames[index] = getDisplayName(evt.name);
|
@@ -401,6 +416,7 @@
|
|
401
416
|
this.dispatchEvent(progressEvent);
|
402
417
|
},
|
403
418
|
onLoadingFinished: (_index, file, glTF) => {
|
419
|
+
if (controller.signal.aborted) return;
|
404
420
|
if (glTF) {
|
405
421
|
loadedFiles.push({
|
406
422
|
src: file,
|
@@ -414,9 +430,14 @@
|
|
414
430
|
if (currentHash !== null && currentHash !== undefined)
|
415
431
|
this._context.hash = currentHash;
|
416
432
|
this._context.alias = alias;
|
417
|
-
|
418
|
-
|
419
|
-
if (debug) console.warn("
|
433
|
+
this._createContextPromise = this._context.create(args);
|
434
|
+
await this._createContextPromise;
|
435
|
+
if (debug) console.warn("--------------\nNeedle Engine: finished loading " + loadId + "\n", filesToLoad, `Aborted? ${controller.signal.aborted}`);
|
436
|
+
if (this._loadId !== loadId || controller.signal.aborted) {
|
437
|
+
console.log("Loading finished but aborted...")
|
438
|
+
return;
|
439
|
+
}
|
440
|
+
|
420
441
|
this._loadingProgress01 = 1;
|
421
442
|
if (useDefaultLoading) {
|
422
443
|
this._loadingView?.onLoadingUpdate(1, "creating scene");
|
@@ -1,6 +1,8 @@
|
|
1
|
-
import { AnimationAction, Euler, Mesh, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, Texture, Uniform, Vector3 } from "three";
|
1
|
+
import { AnimationAction, Box3, Box3Helper, Euler, GridHelper, Mesh, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three";
|
2
2
|
import { ShaderMaterial, WebGLRenderer } from "three";
|
3
|
+
import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
|
3
4
|
|
5
|
+
import { useForAutoFit } from "./engine_camera.js";
|
4
6
|
import { Mathf } from "./engine_math.js"
|
5
7
|
import { CircularBuffer } from "./engine_utils.js";
|
6
8
|
|
@@ -445,3 +447,66 @@
|
|
445
447
|
(typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) ||
|
446
448
|
(typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap);
|
447
449
|
}
|
450
|
+
|
451
|
+
|
452
|
+
|
453
|
+
|
454
|
+
|
455
|
+
export function getBoundingBox(objects: Object3D[], ignore: Object3D[] = []): Box3 {
|
456
|
+
const box = new Box3();
|
457
|
+
box.makeEmpty();
|
458
|
+
|
459
|
+
const emptyChildren = [];
|
460
|
+
function expandByObjectRecursive(obj: Object3D) {
|
461
|
+
let allowExpanding = true;
|
462
|
+
// we dont want to check invisible objects
|
463
|
+
if (!obj.visible) return;
|
464
|
+
if (useForAutoFit(obj) === false) return;
|
465
|
+
if (obj.type === "TransformControlsGizmo" || obj.type === "TransformControlsPlane") return;
|
466
|
+
// ignore Box3Helpers
|
467
|
+
if (obj instanceof Box3Helper) allowExpanding = false;
|
468
|
+
if (obj instanceof GridHelper) allowExpanding = false;
|
469
|
+
// ignore GroundProjectedEnv
|
470
|
+
if (obj instanceof GroundedSkybox) allowExpanding = false;
|
471
|
+
// // Ignore shadow catcher geometry
|
472
|
+
if ((obj as Mesh).material instanceof ShadowMaterial) allowExpanding = false;
|
473
|
+
// ONLY fit meshes
|
474
|
+
if (!(obj instanceof Mesh)) allowExpanding = false;
|
475
|
+
// Ignore things parented to the camera + ignore the camera
|
476
|
+
if (ignore?.includes(obj)) return;
|
477
|
+
// We don't want to fit UI objects
|
478
|
+
if (obj["isUI"] === true) return;
|
479
|
+
// If we encountered some geometry that should be ignored
|
480
|
+
// Then we don't want to use that for expanding the view
|
481
|
+
if (allowExpanding) {
|
482
|
+
// Temporary override children
|
483
|
+
const children = obj.children;
|
484
|
+
obj.children = emptyChildren;
|
485
|
+
// TODO: validate that object doesn't contain NaN values
|
486
|
+
const pos = obj.position;
|
487
|
+
const scale = obj.scale;
|
488
|
+
if (Number.isNaN(pos.x) || Number.isNaN(pos.y) || Number.isNaN(pos.z)) {
|
489
|
+
console.warn(`Object \"${obj.name}\" has NaN values in position or scale.... will ignore it`, pos, scale);
|
490
|
+
return;
|
491
|
+
}
|
492
|
+
box.expandByObject(obj, true);
|
493
|
+
obj.children = children;
|
494
|
+
}
|
495
|
+
for (const child of obj.children) {
|
496
|
+
expandByObjectRecursive(child);
|
497
|
+
}
|
498
|
+
}
|
499
|
+
let hasAnyObject = false;
|
500
|
+
for (const object of objects) {
|
501
|
+
if (!object) continue;
|
502
|
+
hasAnyObject = true;
|
503
|
+
object.updateMatrixWorld();
|
504
|
+
expandByObjectRecursive(object);
|
505
|
+
}
|
506
|
+
if (!hasAnyObject) {
|
507
|
+
console.warn("No objects to fit camera to...");
|
508
|
+
return box;
|
509
|
+
}
|
510
|
+
|
511
|
+
return box;
|
512
|
+
}
|
@@ -1,3 +1,3 @@
|
|
1
|
-
export * from
|
2
|
-
export * from
|
3
|
-
export * from "./webmanifest";
|
1
|
+
export type * from "./needleConfig.d.ts";
|
2
|
+
export type * from './userconfig.d.ts';
|
3
|
+
export type * from "./webmanifest.d.ts";
|
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
|
4
4
|
const $customVisibilityFlag = Symbol("customVisibilityFlag");
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Sets the visibility of a single object without affecting the visibility of the child hierarchy
|
8
|
+
*/
|
5
9
|
export function setCustomVisibility(obj: Object3D, visible: boolean) {
|
6
10
|
obj.layers[$customVisibilityFlag] = visible;
|
7
11
|
}
|
@@ -7,7 +7,7 @@
|
|
7
7
|
import { Mathf } from "../engine/engine_math.js";
|
8
8
|
import { RaycastOptions } from "../engine/engine_physics.js";
|
9
9
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
10
|
-
import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
|
10
|
+
import { getBoundingBox, getWorldDirection, getWorldPosition, getWorldRotation, setWorldRotation } from "../engine/engine_three_utils.js";
|
11
11
|
import type { ICameraController } from "../engine/engine_types.js";
|
12
12
|
import { delayForFrames, getParam, isMobileDevice } from "../engine/engine_utils.js";
|
13
13
|
import { Camera } from "./Camera.js";
|
@@ -607,68 +607,12 @@
|
|
607
607
|
|
608
608
|
const size = new Vector3();
|
609
609
|
const center = new Vector3();
|
610
|
-
const box = new Box3();
|
611
|
-
|
612
610
|
// TODO would be much better to calculate the bounds in camera space instead of world space -
|
613
611
|
// we would get proper view-dependant fit.
|
614
612
|
// Right now it's independent from where the camera is actually looking from,
|
615
613
|
// and thus we're just getting some maximum that will work for sure.
|
614
|
+
const box = getBoundingBox(objects);
|
616
615
|
|
617
|
-
box.makeEmpty();
|
618
|
-
const emptyChildren = [];
|
619
|
-
function expandByObjectRecursive(obj: Object3D) {
|
620
|
-
let allowExpanding = true;
|
621
|
-
// we dont want to check invisible objects
|
622
|
-
if (!obj.visible) return;
|
623
|
-
if (useForAutoFit(obj) === false) return;
|
624
|
-
if(obj.type === "TransformControlsGizmo" || obj.type === "TransformControlsPlane") return;
|
625
|
-
// ignore Box3Helpers
|
626
|
-
if (obj instanceof Box3Helper) allowExpanding = false;
|
627
|
-
if (obj instanceof GridHelper) allowExpanding = false;
|
628
|
-
// ignore GroundProjectedEnv
|
629
|
-
if (obj instanceof GroundedSkybox) allowExpanding = false;
|
630
|
-
// // Ignore shadow catcher geometry
|
631
|
-
if ((obj as Mesh).material instanceof ShadowMaterial) allowExpanding = false;
|
632
|
-
// ONLY fit meshes
|
633
|
-
if (!(obj instanceof Mesh)) allowExpanding = false;
|
634
|
-
// Ignore things parented to the camera + ignore the camera
|
635
|
-
if (obj === camera) return;
|
636
|
-
// We don't want to fit UI objects
|
637
|
-
if (obj["isUI"] === true) return;
|
638
|
-
// If we encountered some geometry that should be ignored
|
639
|
-
// Then we don't want to use that for expanding the view
|
640
|
-
if (allowExpanding) {
|
641
|
-
if (debugCameraFit)
|
642
|
-
console.log(obj.name, obj.type, obj);
|
643
|
-
// Temporary override children
|
644
|
-
const children_length = obj.children;
|
645
|
-
obj.children = emptyChildren;
|
646
|
-
// TODO: validate that object doesn't contain NaN values
|
647
|
-
const pos = obj.position;
|
648
|
-
const scale = obj.scale;
|
649
|
-
if (Number.isNaN(pos.x) || Number.isNaN(pos.y) || Number.isNaN(pos.z)) {
|
650
|
-
console.warn(`Object \"${obj.name}\" has NaN values in position or scale.... will ignore it`, pos, scale);
|
651
|
-
return;
|
652
|
-
}
|
653
|
-
box.expandByObject(obj, true);
|
654
|
-
obj.children = children_length;
|
655
|
-
}
|
656
|
-
for (const child of obj.children) {
|
657
|
-
expandByObjectRecursive(child);
|
658
|
-
}
|
659
|
-
}
|
660
|
-
let hasAnyObject = false;
|
661
|
-
for (const object of objects) {
|
662
|
-
if (!object) continue;
|
663
|
-
hasAnyObject = true;
|
664
|
-
object.updateMatrixWorld();
|
665
|
-
expandByObjectRecursive(object);
|
666
|
-
}
|
667
|
-
if (!hasAnyObject) {
|
668
|
-
console.warn("No objects to fit camera to...");
|
669
|
-
return;
|
670
|
-
}
|
671
|
-
|
672
616
|
camera.updateMatrixWorld();
|
673
617
|
camera.updateProjectionMatrix();
|
674
618
|
box.getCenter(center);
|
@@ -1,5 +1,5 @@
|
|
1
|
+
import { NeedlePWAOptions } from "./webmanifest.d.ts";
|
1
2
|
|
2
|
-
|
3
3
|
export type needleModules = {
|
4
4
|
webpack: object | undefined
|
5
5
|
}
|
@@ -50,11 +50,9 @@
|
|
50
50
|
*/
|
51
51
|
posterGenerationMode?: "default" | "once";
|
52
52
|
|
53
|
-
/**
|
54
|
-
|
55
|
-
|
56
|
-
/** @type {import("vite-plugin-pwa").VitePWAOptions} */
|
57
|
-
pwaOptions?: {};
|
53
|
+
/** Pass in a mix of VitePWA and NeedlePWA options, or "false" */
|
54
|
+
/** @type {import("vite-plugin-pwa").VitePWAOptions & Partial<NeedlePWAOptions> | false} */
|
55
|
+
pwa?: undefined;
|
58
56
|
|
59
57
|
/** used by nextjs config to forward the webpack module */
|
60
58
|
modules: needleModules
|
@@ -3,8 +3,11 @@
|
|
3
3
|
|
4
4
|
declare type Icon = {
|
5
5
|
src: string;
|
6
|
+
/** string with concatenated size options, e.g. "48x48 96x96 192x192" */
|
6
7
|
sizes: string;
|
8
|
+
/** MIME type, e.g. image/svg+xml */
|
7
9
|
type: string;
|
10
|
+
/** Icon purpose, e.g. "any" or "maskable", which have different safe zones. */
|
8
11
|
purpose: string;
|
9
12
|
}
|
10
13
|
|
@@ -12,15 +15,19 @@
|
|
12
15
|
start_url: string;
|
13
16
|
name?: string;
|
14
17
|
short_name?: string;
|
18
|
+
/** Unique ID – make sure that apps that live on the same subdomain use different IDs. */
|
15
19
|
id?: string;
|
16
20
|
icons?: Array<Icon>;
|
21
|
+
/** Workbox config as used by vite-pwa */
|
17
22
|
workbox?: object;
|
18
23
|
}
|
19
24
|
|
20
25
|
export declare type NeedlePWAOptions = {
|
21
|
-
|
26
|
+
/** Update check interval in milliseconds. After this time, a check is done if a new version is available. */
|
27
|
+
updateInterval?: number;
|
22
28
|
}
|
23
29
|
|
24
30
|
export declare type NeedlePWAProcessContext = {
|
31
|
+
/** Temporarily generated files that will be cleaned up after PWA export */
|
25
32
|
generatedFiles: Array<string>;
|
26
33
|
}
|