@@ -1,99 +1,78 @@
|
|
1
1
|
|
2
|
-
import {
|
3
|
-
import { readdirSync, existsSync, readFileSync, copyFileSync, writeFileSync, rmSync, mkdirSync } from 'fs';
|
4
|
-
import { builtAssetsDirectory, getOutputDirectory, tryLoadProjectConfig } from './config.js';
|
2
|
+
import { copyFileSync, existsSync, mkdirSync,readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
5
3
|
|
4
|
+
import { getOutputDirectory } from './config.js';
|
5
|
+
import { getPosterPath } from './poster.js';
|
6
6
|
|
7
7
|
|
8
|
-
/**
|
8
|
+
/** Provides reasonable defaults for a PWA manifest and workbox settings.
|
9
9
|
* @param {import('../types').userSettings} userSettings
|
10
|
-
* @
|
10
|
+
* @param {import("../types/needleConfig").needleMeta | null} config
|
11
|
+
* @param {import("../types/userconfig").userSettings | null} userSettings
|
12
|
+
* @returns {import('vite').Plugin | void}
|
11
13
|
*/
|
12
14
|
export const needlePWA = (command, config, userSettings) => {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
// @ts-ignore // TODO correctly type the userSettings.pwaOptions object
|
16
|
+
/** @type {import("vite-plugin-pwa").VitePWAOptions} */
|
17
|
+
const pwaOptions = userSettings.pwaOptions;
|
18
|
+
|
19
|
+
if (!pwaOptions) {
|
20
|
+
log("WARN: No PWA options found in user settings. Please pass in the pwaOptions to the needlePlugin like so: \"needlePlugins(command, needleConfig, { pwaOptions: PWAOptions })\" and pass the same options to vitePWA plugin like so \"VitePWA(PWAOptions)\"");
|
21
|
+
return cleanup(context);
|
17
22
|
}
|
23
|
+
|
24
|
+
// @ts-ignore // TODO need an extra type so we can add more options into the VitePWAOptions object
|
25
|
+
/** @type {number | undefined} */
|
26
|
+
const updateInterval = pwaOptions.updateInterval || undefined;
|
18
27
|
|
19
|
-
|
28
|
+
if ((command !== "build" && !pwaOptions?.devOptions.enabled) || pwaOptions?.disable) return;
|
29
|
+
if (userSettings?.noPWA === true) return;
|
20
30
|
|
21
|
-
|
22
|
-
|
23
|
-
if (
|
24
|
-
log("No webmanifests found in web project directory or in pwa options that are passed to the needle plugin (via `needlePlugin(command, needleConfig, { pwaOptions: PWAOptions })`)");
|
25
|
-
return;
|
26
|
-
}
|
31
|
+
// The PWA files should not have gzip compression – this will mess up with the precache and the service worker.
|
32
|
+
// If gzip is wanted, the server should serve files with gzip compression on the fly.
|
33
|
+
if (config) config.gzip = false;
|
27
34
|
|
28
35
|
/** The context contains files that are generated by the plugin and should be deleted after
|
29
36
|
* @type {import('../types').NeedlePWAProcessContext} */
|
30
37
|
const context = { generatedFiles: [] }
|
31
38
|
|
32
|
-
if (
|
33
|
-
|
34
|
-
|
35
|
-
const manifestName = "needle.generated.webmanifest";
|
36
|
-
const tempManifestPath = currentDir + "/" + manifestName;
|
37
|
-
const content = JSON.stringify(pwaOptions.manifest || {});
|
38
|
-
writeFileSync(tempManifestPath, content, 'utf8');
|
39
|
-
context.generatedFiles.push(tempManifestPath);
|
40
|
-
manifests.push(manifestName);
|
39
|
+
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");
|
41
|
+
pwaOptions.registerType = "autoUpdate";
|
41
42
|
}
|
43
|
+
if (!pwaOptions.outDir) {
|
44
|
+
const outDir = getOutputDirectory();
|
45
|
+
log("Set PWA outDir to " + outDir);
|
46
|
+
pwaOptions.outDir = outDir;
|
47
|
+
}
|
42
48
|
|
43
|
-
|
44
|
-
|
45
|
-
|
49
|
+
const currentDir = process.cwd();
|
50
|
+
|
51
|
+
/** @type {Array<string | Partial<import("vite-plugin-pwa").ManifestOptions>>} */
|
52
|
+
const manifests = readdirSync(currentDir).filter(f => f.endsWith(".webmanifest")) || [];
|
53
|
+
|
54
|
+
if (manifests.length > 1) {
|
55
|
+
log("WARN: Multiple webmanifests found in web project directory. Only the first one will be processed: ", manifests[0]);
|
46
56
|
}
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
}
|
52
|
-
if (!pwaOptions.outDir) {
|
53
|
-
const outDir = getOutputDirectory();
|
54
|
-
log("Set PWA outDir to " + outDir);
|
55
|
-
pwaOptions.outDir = outDir;
|
56
|
-
}
|
57
|
+
|
58
|
+
if (manifests.length <= 0 && !pwaOptions) {
|
59
|
+
log("No webmanifests found in web project directory or in pwa options that are passed to the needle plugin (via `needlePlugin(command, needleConfig, { pwaOptions: PWAOptions })`)");
|
60
|
+
return;
|
57
61
|
}
|
58
62
|
|
59
|
-
|
60
|
-
/** @type {Array<{path:string, copypath:string}>} */
|
61
|
-
const originalManifestContents = manifests.map(filename => {
|
62
|
-
const path = currentDir + "/" + filename;
|
63
|
-
const backupDir = currentDir + "/node_modules/.needle";
|
64
|
-
if (!existsSync(backupDir)) {
|
65
|
-
mkdirSync(backupDir, { recursive: true });
|
66
|
-
}
|
67
|
-
const copyPath = backupDir + "/" + filename + ".original";
|
68
|
-
log("Backup original webmanifest", path, "to", copyPath);
|
69
|
-
copyFileSync(path, copyPath);
|
70
|
-
return { path, copyPath };
|
71
|
-
});
|
63
|
+
const customManifest = manifests.length > 0 ? manifests[0] : {};
|
72
64
|
|
65
|
+
// ensure we have proper icons/name/description to match user settings
|
66
|
+
processPWA(customManifest, context, pwaOptions, config, userSettings).catch(e => log("Error post processing PWA", customManifest, e));
|
67
|
+
// ensures we have a valid workbox config
|
68
|
+
processWorkboxConfig(pwaOptions);
|
73
69
|
|
74
|
-
// modify the manifests so they're loaded via VitePlugin
|
75
|
-
for (const manifest of manifests) {
|
76
|
-
try {
|
77
|
-
processPWA(manifest, context).catch(e => log("Error post processing PWA", manifest, e));
|
78
|
-
log("Assign PWA manifest to VitePWA plugin: \"" + manifest + "\"");
|
79
|
-
pwaOptions.manifest = {
|
80
|
-
start_url: "./index.html",
|
81
|
-
...JSON.parse(readFileSync(manifest, 'utf8')),
|
82
|
-
...pwaOptions.manifest
|
83
|
-
};
|
84
|
-
// we only want to process the first manifest
|
85
|
-
break;
|
86
|
-
}
|
87
|
-
catch (e) {
|
88
|
-
log("Error post processing PWA", manifest, e);
|
89
|
-
restoreOriginalManifests(originalManifestContents);
|
90
|
-
break;
|
91
|
-
}
|
92
|
-
}
|
93
|
-
|
94
70
|
/** @type {Array<string>} */
|
95
71
|
const postBuildMessages = [];
|
96
72
|
|
73
|
+
// for debugging
|
74
|
+
// log("PWA options", pwaOptions);
|
75
|
+
|
97
76
|
return {
|
98
77
|
name: 'needle-pwa',
|
99
78
|
apply: 'build',
|
@@ -108,7 +87,7 @@
|
|
108
87
|
if (existsSync(indexPath)) {
|
109
88
|
const indexContent = readFileSync(indexPath, 'utf8');
|
110
89
|
if (indexContent.includes(".webmanifest")) {
|
111
|
-
throw new Error("ERR: [needle-pwa] index.html contains a reference to a webmanifest. This is not supported
|
90
|
+
throw new Error("ERR: [needle-pwa] index.html contains a reference to a webmanifest. This is currently not supported. Please remove the reference from the index.html, or remove the needle-pwa plugin and manage PWA building yourself.");
|
112
91
|
}
|
113
92
|
}
|
114
93
|
}
|
@@ -116,9 +95,6 @@
|
|
116
95
|
cleanup(context);
|
117
96
|
throw err;
|
118
97
|
}
|
119
|
-
finally {
|
120
|
-
restoreOriginalManifests(originalManifestContents);
|
121
|
-
}
|
122
98
|
},
|
123
99
|
transformIndexHtml: {
|
124
100
|
enforce: 'pre',
|
@@ -126,10 +102,59 @@
|
|
126
102
|
// see https://vite-pwa-org.netlify.app/guide/auto-update.html
|
127
103
|
// post transform so we want to linebreak after the vite logs
|
128
104
|
console.log("\n");
|
129
|
-
const scriptContent = `//
|
130
|
-
import { registerSW } from 'virtual:pwa-register'
|
131
|
-
|
132
|
-
|
105
|
+
const scriptContent = `// 🌵 Added by the [needle-pwa] plugin.
|
106
|
+
import { registerSW } from 'virtual:pwa-register';
|
107
|
+
const debugServiceWorker = new URLSearchParams(window.location.search).get('debugpwa') !== null;
|
108
|
+
${updateInterval !== undefined ? `const intervalMS = ${updateInterval};` : ''}
|
109
|
+
let updateSW;
|
110
|
+
updateSW = registerSW({
|
111
|
+
immediate: true,
|
112
|
+
onRegisteredSW(swUrl, r) {
|
113
|
+
// console.log('[needle-pwa] Service worker for PWA support has been registered.');
|
114
|
+
${updateInterval !== undefined ? `
|
115
|
+
r && setInterval(async () => {
|
116
|
+
if (debugServiceWorker) console.log('[needle-pwa] Checking for updates...');
|
117
|
+
|
118
|
+
if (!(!r.installing && navigator)) {
|
119
|
+
if (debugServiceWorker) console.log('[needle-pwa] Service worker is not ready yet');
|
120
|
+
return;
|
121
|
+
}
|
122
|
+
|
123
|
+
// navigator.onLine is currently broken in Chrome
|
124
|
+
/*
|
125
|
+
if (('connection' in navigator) && !navigator.onLine) {
|
126
|
+
// console.log('[needle-pwa] Service worker update check skipped – offline');
|
127
|
+
return;
|
128
|
+
}
|
129
|
+
*/
|
130
|
+
|
131
|
+
const resp = await fetch(swUrl, {
|
132
|
+
cache: 'no-store',
|
133
|
+
headers: {
|
134
|
+
'cache': 'no-store',
|
135
|
+
'cache-control': 'no-cache',
|
136
|
+
},
|
137
|
+
});
|
138
|
+
|
139
|
+
if (resp?.status === 200) {
|
140
|
+
// if there's actually an update, onNeedRefresh will be called.
|
141
|
+
await r.update();
|
142
|
+
}
|
143
|
+
}, intervalMS);
|
144
|
+
` : ``}
|
145
|
+
},
|
146
|
+
${updateInterval !== undefined ? `
|
147
|
+
onNeedRefresh() {
|
148
|
+
if (debugServiceWorker) console.log('[needle-pwa] The web app has been updated in the background. Refreshing.');
|
149
|
+
updateSW(true);
|
150
|
+
},
|
151
|
+
` : ``}
|
152
|
+
onOfflineReady() {
|
153
|
+
if (debugServiceWorker) console.log('[needle-pwa] This web app is now installed and ready to be used offline.');
|
154
|
+
},
|
155
|
+
});
|
156
|
+
`;
|
157
|
+
log("Added PWA registration script to index.html." + (updateInterval !== undefined ? " Auto update interval is set to " + updateInterval + "ms." : ""));
|
133
158
|
return {
|
134
159
|
html,
|
135
160
|
tags: [
|
@@ -146,16 +171,12 @@
|
|
146
171
|
closeBundle() {
|
147
172
|
// copy the icons to the output directory
|
148
173
|
const outputDir = getOutputDirectory();
|
149
|
-
|
150
|
-
|
151
|
-
try {
|
152
|
-
const manifest = JSON.parse(readFileSync(webmanifestPath, 'utf8'));
|
153
|
-
copyIcons(manifest, outputDir);
|
154
|
-
}
|
155
|
-
catch (e) {
|
156
|
-
log("Error post processing PWA", webmanifestPath, e);
|
157
|
-
}
|
174
|
+
try {
|
175
|
+
copyIcons(pwaOptions.manifest, outputDir);
|
158
176
|
}
|
177
|
+
catch (e) {
|
178
|
+
log("Error post processing PWA", e);
|
179
|
+
}
|
159
180
|
|
160
181
|
cleanup(context);
|
161
182
|
|
@@ -200,80 +221,93 @@
|
|
200
221
|
context.generatedFiles.length = 0;
|
201
222
|
}
|
202
223
|
|
203
|
-
/**
|
204
|
-
* @param {Array<{path:string, copypath:string}>} manifests
|
205
|
-
*/
|
206
|
-
function restoreOriginalManifests(manifests) {
|
207
|
-
// restore the manifest
|
208
|
-
for (const manifest of manifests) {
|
209
|
-
try {
|
210
|
-
log("Restore original webmanifest", manifest.path);
|
211
|
-
copyFileSync(manifest.copyPath, manifest.path);
|
212
|
-
rmSync(manifest.copyPath);
|
213
|
-
}
|
214
|
-
catch (e) {
|
215
|
-
log("Error restoring original webmanifest", manifest, e);
|
216
|
-
}
|
217
|
-
}
|
218
|
-
manifests.length = 0;
|
219
|
-
}
|
220
|
-
|
221
|
-
|
222
224
|
function log(...args) {
|
223
225
|
console.log("[needle-pwa]", ...args);
|
224
226
|
}
|
225
|
-
function delay(ms) {
|
226
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
227
|
-
}
|
228
227
|
|
229
228
|
/**
|
230
|
-
* @param {string} webmanifestPath
|
229
|
+
* @param {string | Partial<import("vite-plugin-pwa").ManifestOptions>} webmanifestPath Path to the webmanifest file, or partial manifest itself
|
231
230
|
* @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
|
232
|
-
* @param {import("
|
231
|
+
* @param {import("vite-plugin-pwa").VitePWAOptions} pwaOptions
|
232
|
+
* @param {import("../types/needleConfig").needleMeta | null} config
|
233
|
+
* @param {import("../types/userconfig").userSettings | null} userSettings
|
233
234
|
*/
|
234
|
-
async function processPWA(webmanifestPath, context,
|
235
|
+
async function processPWA(webmanifestPath, context, pwaOptions, config, userSettings) {
|
235
236
|
const outDir = getOutputDirectory();
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
237
|
+
|
238
|
+
// if a path is provided, we read that and treat it as manifest
|
239
|
+
// if an object is provided, we treat it as partial manifest
|
240
|
+
|
241
|
+
/** @type {Partial<import("vite-plugin-pwa").ManifestOptions>} */
|
242
|
+
let manifest = {};
|
243
|
+
|
244
|
+
if (typeof webmanifestPath === "string") {
|
245
|
+
manifest = JSON.parse(readFileSync(webmanifestPath, 'utf8'));
|
242
246
|
}
|
247
|
+
else {
|
248
|
+
manifest = webmanifestPath || {};
|
249
|
+
}
|
243
250
|
|
244
|
-
|
245
|
-
const manifest = JSON.parse(readFileSync(webmanifestPath, 'utf8'));
|
246
|
-
let modifiedManifest = false;
|
251
|
+
processIcons(manifest, outDir, context);
|
247
252
|
|
248
|
-
if (processIcons(manifest, outDir, context) === true) modifiedManifest = true;
|
249
|
-
if (processWorkboxConfig(manifest) === true) modifiedManifest = true;
|
250
|
-
|
251
253
|
const packageJsonPath = process.cwd() + "/package.json";
|
252
254
|
const packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : {};
|
253
255
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
256
|
+
const name = packageJson.name;
|
257
|
+
const appName = config?.sceneName || packageJson.name || "Needle App";
|
258
|
+
|
259
|
+
/** @type {Partial<import("vite-plugin-pwa").ManifestOptions>} */
|
260
|
+
const defaultManifest = {
|
261
|
+
// Use the same title as in NeedleMeta
|
262
|
+
name: appName,
|
263
|
+
short_name: appName,
|
264
|
+
id: "app.made-with-needle." + name.toLowerCase().replace(/[^a-z0-9\/\-]/g, '').replace("\/", "."),
|
265
|
+
description: config.description || packageJson.description || "Made with Needle Engine",
|
266
|
+
start_url: "./index.html",
|
267
|
+
display: "standalone",
|
268
|
+
display_override: [
|
269
|
+
"window-controls-overlay",
|
270
|
+
"standalone",
|
271
|
+
"browser"
|
272
|
+
],
|
273
|
+
dir: "ltr",
|
274
|
+
lang: "en",
|
275
|
+
related_applications: [],
|
276
|
+
prefer_related_applications: false,
|
277
|
+
categories: ["spatial", "3D", "needle", "webgl", "webxr", "xr"],
|
278
|
+
};
|
279
|
+
|
280
|
+
// Use the poster image if that exists
|
281
|
+
if (!userSettings.noPoster) {
|
282
|
+
const posterPath = getPosterPath();
|
283
|
+
defaultManifest.screenshots = [{
|
284
|
+
src: "./" + posterPath,
|
285
|
+
type: "image/webp",
|
286
|
+
sizes: "1080x1080", // TODO get actual size
|
287
|
+
form_factor: "wide",
|
288
|
+
}];
|
260
289
|
}
|
261
|
-
if (manifest.description === undefined) {
|
262
|
-
manifest.description = packageJson.description || "Made with Needle Engine";
|
263
|
-
if (manifest.description?.length)
|
264
|
-
modifiedManifest = true;
|
265
|
-
}
|
266
290
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
291
|
+
// display override and window controls override, for native look and feel
|
292
|
+
if (defaultManifest.display === undefined) defaultManifest.display = "standalone";
|
293
|
+
if (defaultManifest.display_override === undefined) defaultManifest.display_override = [
|
294
|
+
"window-controls-overlay",
|
295
|
+
"standalone",
|
296
|
+
"browser"
|
297
|
+
];
|
298
|
+
|
299
|
+
// Merge into pwaOptions. start_url is important, and
|
300
|
+
// we also want to keep whatever is already in the pwaOptions.manifest object
|
301
|
+
pwaOptions.manifest = {
|
302
|
+
...defaultManifest,
|
303
|
+
...manifest,
|
304
|
+
...pwaOptions.manifest
|
305
|
+
};
|
272
306
|
}
|
273
307
|
|
274
308
|
/** Copies icons to the output directory
|
275
309
|
* If a start_url is provided, it will be used as the base path for relative icon paths
|
276
|
-
* @param {import("
|
310
|
+
* @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
|
277
311
|
* @param {string} outDir
|
278
312
|
* @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
|
279
313
|
* @returns {boolean} */
|
@@ -285,10 +319,12 @@
|
|
285
319
|
generateIcons(manifest, context);
|
286
320
|
}
|
287
321
|
|
322
|
+
if (!manifest.icons) return modified;
|
323
|
+
|
288
324
|
for (let i = 0; i < manifest.icons?.length; i++) {
|
289
325
|
try {
|
290
326
|
const icon = manifest.icons[i];
|
291
|
-
|
327
|
+
const src = icon.src;
|
292
328
|
const iconSrcIsAbsolute = src.startsWith("http://") || src.startsWith("https://");
|
293
329
|
const hasAbsoluteStartUrl = manifest.start_url?.startsWith("http://") || manifest.start_url?.startsWith("https://");
|
294
330
|
if (!iconSrcIsAbsolute && hasAbsoluteStartUrl) {
|
@@ -305,7 +341,7 @@
|
|
305
341
|
}
|
306
342
|
|
307
343
|
/**
|
308
|
-
* @param {import("
|
344
|
+
* @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
|
309
345
|
* @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
|
310
346
|
*/
|
311
347
|
function generateIcons(manifest, context) {
|
@@ -314,7 +350,7 @@
|
|
314
350
|
const sizes = [192, 512];
|
315
351
|
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>`;
|
316
352
|
const iconDir = process.cwd();
|
317
|
-
for (
|
353
|
+
for (const size of sizes) {
|
318
354
|
const iconName = "pwa-icon-" + size + ".svg";
|
319
355
|
const iconPath = iconDir + "/" + iconName;
|
320
356
|
writeFileSync(iconPath, defaultIconSVG, 'utf8');
|
@@ -336,6 +372,7 @@
|
|
336
372
|
}
|
337
373
|
|
338
374
|
/** Tries to copy the icons to the output directory
|
375
|
+
* @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
|
339
376
|
*/
|
340
377
|
function copyIcons(manifest, outDir) {
|
341
378
|
for (let i = 0; i < manifest.icons?.length; i++) {
|
@@ -378,13 +415,14 @@
|
|
378
415
|
|
379
416
|
/**
|
380
417
|
* Merges the current workbox config with the default workbox config
|
381
|
-
* @param {import("
|
418
|
+
* @param {import("vite-plugin-pwa").VitePWAOptions} manifest
|
382
419
|
*/
|
383
420
|
function processWorkboxConfig(manifest) {
|
384
421
|
|
385
422
|
// Workaround: urlPattern, ignoreSearch und ignoreURLParametersMatching, dontCacheBustURLsMatching are because we currently append ?v=... to loaded GLB files
|
386
423
|
|
387
424
|
// this is our default config
|
425
|
+
/** @type {Partial<import("workbox-build").GenerateSWOptions>} */
|
388
426
|
const defaultWorkboxConfig = {
|
389
427
|
globPatterns: ['**'],
|
390
428
|
// glb files are large – we need to increase the cache size here.
|
@@ -407,12 +445,12 @@
|
|
407
445
|
],
|
408
446
|
}
|
409
447
|
|
410
|
-
if (manifest.
|
411
|
-
manifest.
|
448
|
+
if (manifest.workbox) {
|
449
|
+
manifest.workbox = { ...defaultWorkboxConfig, ...manifest.workbox };
|
412
450
|
return true;
|
413
451
|
}
|
414
452
|
else {
|
415
|
-
manifest.
|
453
|
+
manifest.workbox = defaultWorkboxConfig;
|
416
454
|
return true;
|
417
455
|
}
|
418
456
|
}
|
@@ -333,6 +333,9 @@
|
|
333
333
|
}
|
334
334
|
|
335
335
|
private setStartTransition() {
|
336
|
+
if (this.model.layers.length > 1 && (debug || isDevEnvironment())) {
|
337
|
+
console.warn("Multiple layers are not supported yet " + this.animator?.name);
|
338
|
+
}
|
336
339
|
for (const layer of this.model.layers) {
|
337
340
|
const sm = layer.stateMachine;
|
338
341
|
if (sm.defaultState === undefined) {
|
@@ -342,6 +345,7 @@
|
|
342
345
|
}
|
343
346
|
const start = sm.states[sm.defaultState];
|
344
347
|
this.transitionTo(start, 0, this.normalizedStartOffset);
|
348
|
+
break;
|
345
349
|
}
|
346
350
|
}
|
347
351
|
|
@@ -380,11 +384,16 @@
|
|
380
384
|
if (action) {
|
381
385
|
const dur = state.motion.clip!.duration;
|
382
386
|
const normalizedTime = dur <= 0 ? 1 : Math.abs(action.time / dur);
|
387
|
+
let exitTime = transition.exitTime;
|
388
|
+
|
389
|
+
// When the animation is playing backwards we need to check exit time inverted
|
390
|
+
if (action.timeScale < 0) {
|
391
|
+
exitTime = 1 - exitTime;
|
392
|
+
}
|
393
|
+
|
383
394
|
let makeTransition = false;
|
384
395
|
if (transition.hasExitTime) {
|
385
|
-
|
386
|
-
// When the animation is playing backwards we need to check exit time inverted
|
387
|
-
else if (action.timeScale < 0) makeTransition = 1 - normalizedTime >= transition.exitTime;
|
396
|
+
makeTransition = normalizedTime >= exitTime;
|
388
397
|
}
|
389
398
|
else {
|
390
399
|
makeTransition = true;
|
@@ -403,7 +412,8 @@
|
|
403
412
|
action.clampWhenFinished = true;
|
404
413
|
// else action.clampWhenFinished = false;
|
405
414
|
if (debug) {
|
406
|
-
|
415
|
+
const targetState = this.getState(transition.destinationState, 0);
|
416
|
+
console.log(`Transition to ${transition.destinationState} / ${targetState?.name}`, transition, "\nTimescale: " + action.timeScale, "\nNormalized time: " + normalizedTime.toFixed(3), "\nExit Time: " + exitTime, transition.hasExitTime);
|
407
417
|
// console.log(action.time, transition.exitTime);
|
408
418
|
}
|
409
419
|
this.transitionTo(transition.destinationState, transition.duration, transition.offset);
|
@@ -423,12 +433,7 @@
|
|
423
433
|
|
424
434
|
// update timescale
|
425
435
|
if (action) {
|
426
|
-
|
427
|
-
if (state.speedParameter)
|
428
|
-
speedFactor *= this.getFloat(state.speedParameter);
|
429
|
-
if (speedFactor !== undefined) {
|
430
|
-
action.timeScale = speedFactor * this._speed;
|
431
|
-
}
|
436
|
+
this.setTimescale(action, state);
|
432
437
|
}
|
433
438
|
|
434
439
|
let didTriggerLooping = false;
|
@@ -466,6 +471,15 @@
|
|
466
471
|
|
467
472
|
}
|
468
473
|
|
474
|
+
private setTimescale(action: AnimationAction, state: State) {
|
475
|
+
let speedFactor = state.speed ?? 1;
|
476
|
+
if (state.speedParameter)
|
477
|
+
speedFactor *= this.getFloat(state.speedParameter);
|
478
|
+
if (speedFactor !== undefined) {
|
479
|
+
action.timeScale = speedFactor * this._speed;
|
480
|
+
}
|
481
|
+
}
|
482
|
+
|
469
483
|
private getState(state: State | number, layerIndex: number): State | null {
|
470
484
|
if (typeof state === "number") {
|
471
485
|
if (state == -1) {
|
@@ -481,6 +495,17 @@
|
|
481
495
|
return state;
|
482
496
|
}
|
483
497
|
|
498
|
+
/**
|
499
|
+
* These actions have been active previously but not faded out because we entered a state that has no real animation - no duration. In which case we hold the previously active actions until they are faded out.
|
500
|
+
*/
|
501
|
+
private readonly _heldActions: AnimationAction[] = [];
|
502
|
+
private releaseHeldActions(duration: number) {
|
503
|
+
for (const prev of this._heldActions) {
|
504
|
+
prev.fadeOut(duration);
|
505
|
+
}
|
506
|
+
this._heldActions.length = 0;
|
507
|
+
}
|
508
|
+
|
484
509
|
private transitionTo(state: State | number, durationInSec: number, offsetNormalized: number) {
|
485
510
|
|
486
511
|
if (!this.animator) return;
|
@@ -519,19 +544,31 @@
|
|
519
544
|
}
|
520
545
|
|
521
546
|
const prevAction = this._activeState?.motion.action;
|
522
|
-
|
523
|
-
|
524
|
-
}
|
547
|
+
|
548
|
+
|
525
549
|
if (isSelf) {
|
526
550
|
state.motion.action = state.motion.action_loopback;
|
527
551
|
state.motion.action_loopback = prevAction;
|
528
552
|
}
|
529
553
|
const prev = this._activeState;
|
530
554
|
this._activeState = state;
|
555
|
+
const action = state.motion?.action;
|
531
556
|
|
532
|
-
const
|
557
|
+
const clip = state.motion.clip;
|
558
|
+
|
559
|
+
|
560
|
+
if (clip?.duration <= 0 && clip.tracks.length <= 0) {
|
561
|
+
// if the new state doesn't have a valid clip / no tracks we don't fadeout the previous action and instead hold the previous action.
|
562
|
+
if (prevAction) {
|
563
|
+
this._heldActions.push(prevAction);
|
564
|
+
}
|
565
|
+
}
|
566
|
+
else if (prevAction) {
|
567
|
+
prevAction!.fadeOut(durationInSec);
|
568
|
+
this.releaseHeldActions(durationInSec);
|
569
|
+
}
|
570
|
+
|
533
571
|
if (action) {
|
534
|
-
|
535
572
|
offsetNormalized = Math.max(0, Math.min(1, offsetNormalized));
|
536
573
|
if (state.cycleOffsetParameter) {
|
537
574
|
let val = this.getFloat(state.cycleOffsetParameter);
|
@@ -550,6 +587,7 @@
|
|
550
587
|
action.stop();
|
551
588
|
action.reset();
|
552
589
|
action.enabled = true;
|
590
|
+
this.setTimescale(action, state);
|
553
591
|
const duration = state.motion.clip!.duration;
|
554
592
|
// if we are looping to the same state we don't want to offset the current start time
|
555
593
|
action.time = isSelf ? 0 : offsetNormalized * duration;
|
@@ -649,6 +687,7 @@
|
|
649
687
|
|
650
688
|
// ensure we have a motion even if none was exported
|
651
689
|
if (!state.motion) {
|
690
|
+
if (debug) console.warn("No motion", state);
|
652
691
|
state.motion = createMotion(state.name);
|
653
692
|
// console.warn("Missing motion", "AnimatorController: " + this.model.name, state);
|
654
693
|
// sm.states.splice(index, 1);
|
@@ -670,6 +709,7 @@
|
|
670
709
|
|
671
710
|
// ensure we have a clip to blend to
|
672
711
|
if (!state.motion.clip) {
|
712
|
+
if (debug) console.warn("No clip assigned to state", state);
|
673
713
|
const clip = new AnimationClip(undefined, undefined, []);
|
674
714
|
state.motion.clip = clip;
|
675
715
|
}
|
@@ -686,6 +686,7 @@
|
|
686
686
|
window.addEventListener("touchend", this.onTouchEnd, { passive: true });
|
687
687
|
|
688
688
|
this._htmlEventSource.addEventListener('wheel', this.onMouseWheel, { passive: true });
|
689
|
+
window.addEventListener("wheel", this.onWheelWindow, { passive: true })
|
689
690
|
|
690
691
|
window.addEventListener("keydown", this.onKeyDown, false);
|
691
692
|
window.addEventListener("keypress", this.onKeyPressed, false);
|
@@ -704,6 +705,7 @@
|
|
704
705
|
window.removeEventListener('pointercancel', this.onPointerCancel);
|
705
706
|
|
706
707
|
this._htmlEventSource?.removeEventListener('wheel', this.onMouseWheel, false);
|
708
|
+
window.removeEventListener("wheel", this.onWheelWindow, false);
|
707
709
|
|
708
710
|
window.removeEventListener("keydown", this.onKeyDown, false);
|
709
711
|
window.removeEventListener("keypress", this.onKeyPressed, false);
|
@@ -818,6 +820,13 @@
|
|
818
820
|
this.onDispatchEvent(ne);
|
819
821
|
}
|
820
822
|
|
823
|
+
private onWheelWindow = (evt: WheelEvent) => {
|
824
|
+
// check if we are in pointer lock mode
|
825
|
+
if (document.pointerLockElement) {
|
826
|
+
// only if yes we want to use the mouse wheel as a pointer event
|
827
|
+
this.onMouseWheel(evt);
|
828
|
+
}
|
829
|
+
};
|
821
830
|
private onMouseWheel = (evt: WheelEvent) => {
|
822
831
|
if (this.canReceiveInput(evt) === false) return;
|
823
832
|
if (this._mouseWheelDeltaY.length <= 0) this._mouseWheelDeltaY.push(0);
|
@@ -942,12 +951,12 @@
|
|
942
951
|
}
|
943
952
|
|
944
953
|
// Prevent the same event being handled twice (e.g. on touch we get a mouseUp and touchUp evt with the same timestamp)
|
945
|
-
private isNewEvent(timestamp: number, index: number, arr: number[]): boolean {
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
}
|
954
|
+
// private isNewEvent(timestamp: number, index: number, arr: number[]): boolean {
|
955
|
+
// while (arr.length <= index) arr.push(-1);
|
956
|
+
// if (timestamp === arr[index]) return false;
|
957
|
+
// arr[index] = timestamp;
|
958
|
+
// return true;
|
959
|
+
// }
|
951
960
|
|
952
961
|
private isInRect(e: { clientX: number, clientY: number }): boolean {
|
953
962
|
if (this.context.isInXR) return true;
|
@@ -11,20 +11,17 @@
|
|
11
11
|
import { Context } from "./engine_setup.js"
|
12
12
|
import { type SourceIdentifier, type UIDProvider } from "./engine_types.js";
|
13
13
|
import * as utils from "./engine_utils.js";
|
14
|
-
import { registerComponentExtension, registerExtensions } from "./extensions/extensions.js";
|
14
|
+
import { invokeAfterImportPluginHooks, registerComponentExtension, registerExtensions } from "./extensions/extensions.js";
|
15
15
|
import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
|
16
16
|
|
17
|
-
|
18
17
|
/** @internal */
|
19
18
|
export class NeedleGltfLoader implements INeedleGltfLoader {
|
20
19
|
createBuiltinComponents(context: Context, gltfId: string, gltf: any, seed: number | UIDProvider | null, extension?: NEEDLE_components | undefined) {
|
21
20
|
return createBuiltinComponents(context, gltfId, gltf, seed, extension);
|
22
21
|
}
|
23
|
-
|
24
22
|
writeBuiltinComponentData(comp: any, context: SerializationContext) {
|
25
23
|
return writeBuiltinComponentData(comp, context);
|
26
24
|
}
|
27
|
-
|
28
25
|
parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
|
29
26
|
return parseSync(context, data, path, seed);
|
30
27
|
}
|
@@ -32,7 +29,6 @@
|
|
32
29
|
return loadSync(context, url, sourceId, seed, prog);
|
33
30
|
}
|
34
31
|
}
|
35
|
-
|
36
32
|
registerLoader(NeedleGltfLoader);
|
37
33
|
|
38
34
|
|
@@ -53,7 +49,6 @@
|
|
53
49
|
loader: GLTFLoader;
|
54
50
|
path: string;
|
55
51
|
gltf?: GLTF;
|
56
|
-
|
57
52
|
constructor(context: Context, path: string, loader: GLTFLoader, gltf?: GLTF) {
|
58
53
|
this.context = context;
|
59
54
|
this.path = path;
|
@@ -99,8 +94,7 @@
|
|
99
94
|
|
100
95
|
export async function createGLTFLoader(url: string, context: Context) {
|
101
96
|
const loader = new GLTFLoader();
|
102
|
-
|
103
|
-
await registerExtensions(loader, context, sourceId);
|
97
|
+
await registerExtensions(loader, context, url);
|
104
98
|
return loader;
|
105
99
|
}
|
106
100
|
|
@@ -139,6 +133,7 @@
|
|
139
133
|
invokeEvents(GltfLoadEventType.BeforeLoad, new GltfLoadEvent(context, path, loader));
|
140
134
|
const camera = context.mainCamera;
|
141
135
|
loader.parse(data, "", async res => {
|
136
|
+
invokeAfterImportPluginHooks(path, res, context);
|
142
137
|
invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, path, loader, res));
|
143
138
|
await handleLoadedGltf(context, path, res, seed, componentsExtension);
|
144
139
|
await compileAsync(res.scene, context, camera);
|
@@ -185,6 +180,7 @@
|
|
185
180
|
invokeEvents(GltfLoadEventType.BeforeLoad, new GltfLoadEvent(context, url, loader));
|
186
181
|
const camera = context.mainCamera;
|
187
182
|
loader.load(url, async res => {
|
183
|
+
invokeAfterImportPluginHooks(url, res, context);
|
188
184
|
invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, url, loader, res));
|
189
185
|
await handleLoadedGltf(context, sourceId, res, seed, componentsExtension);
|
190
186
|
await compileAsync(res.scene, context, camera);
|
@@ -240,30 +236,3 @@
|
|
240
236
|
a.click();
|
241
237
|
}
|
242
238
|
}
|
243
|
-
|
244
|
-
// TODO: save references in guid map
|
245
|
-
// const guidMap = {};
|
246
|
-
|
247
|
-
|
248
|
-
/** @deprecated */
|
249
|
-
export function tryFindObjectByName(name: string, obj: Object3D, recursive = true) {
|
250
|
-
if (obj.userData && obj.userData.name === name) return obj;
|
251
|
-
if (obj.children && obj.children.length > 0) {
|
252
|
-
for (let i = 0; i < obj.children.length; i++) {
|
253
|
-
const child = obj.children[i];
|
254
|
-
const found = tryFindObjectByName(name, child, recursive);
|
255
|
-
if (found) return found;
|
256
|
-
}
|
257
|
-
}
|
258
|
-
}
|
259
|
-
|
260
|
-
/** @deprecated */
|
261
|
-
export function tryFindScript(globalObjectIdentifier: string, list = null) {
|
262
|
-
const arr = list ?? Context.Current.scripts;
|
263
|
-
for (const i in arr) {
|
264
|
-
const script = arr[i];
|
265
|
-
if (script && script.guid === globalObjectIdentifier)
|
266
|
-
return script;
|
267
|
-
}
|
268
|
-
return null;
|
269
|
-
}
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { isDevEnvironment } from "../debug/index.js";
|
5
5
|
import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
|
6
6
|
import { Context } from "../engine_setup.js";
|
7
|
-
import { type ConstructorConcrete, type SourceIdentifier } from "../engine_types.js";
|
7
|
+
import { type ConstructorConcrete, GLTF, type SourceIdentifier } from "../engine_types.js";
|
8
8
|
import { getParam } from "../engine_utils.js";
|
9
9
|
import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
|
10
10
|
import { EXT_texture_exr } from "./EXT_texture_exr.js";
|
@@ -29,15 +29,28 @@
|
|
29
29
|
});
|
30
30
|
|
31
31
|
|
32
|
-
declare type OnImportCallback = (loader: GLTFLoader,
|
33
|
-
declare type OnExportCallback = (
|
32
|
+
declare type OnImportCallback = (loader: GLTFLoader, url: string, context: Context) => void;
|
33
|
+
declare type OnExportCallback = (exporter: GLTFExporter, context: Context) => void;
|
34
34
|
|
35
|
+
/**
|
36
|
+
* Interface for registering custom glTF extensions to the Needle Engine GLTFLoaders. Register your plugin via {@link addCustomExtensionPlugin}
|
37
|
+
*/
|
35
38
|
export interface INeedleGLTFExtensionPlugin {
|
39
|
+
/** The Name of your plugin */
|
36
40
|
name: string;
|
41
|
+
/** Called before starting to load a glTF file. This callback can be used to add custom extensions to the GLTFLoader */
|
37
42
|
onImport?: OnImportCallback;
|
38
|
-
|
43
|
+
/** Called after the glTF has been loaded */
|
44
|
+
onLoaded?: (url: string, gltf: GLTF, context: Context) => void;
|
45
|
+
/** Called before starting to export a glTF file. This callback can be used to add custom extensions to the GLTFExporter */
|
46
|
+
onExport?: OnExportCallback;
|
39
47
|
}
|
40
48
|
|
49
|
+
/**
|
50
|
+
* Registered custom glTF extension plugins
|
51
|
+
*/
|
52
|
+
const _plugins = new Array<INeedleGLTFExtensionPlugin>();
|
53
|
+
|
41
54
|
/** Register callbacks for registering custom gltf importer or exporter plugins */
|
42
55
|
export function addCustomExtensionPlugin(ext: INeedleGLTFExtensionPlugin) {
|
43
56
|
if (!_plugins.includes(ext)) {
|
@@ -50,7 +63,6 @@
|
|
50
63
|
if (index >= 0)
|
51
64
|
_plugins.splice(index, 1);
|
52
65
|
}
|
53
|
-
const _plugins = new Array<INeedleGLTFExtensionPlugin>();
|
54
66
|
|
55
67
|
|
56
68
|
/** Registers the Needle Engine components extension */
|
@@ -75,27 +87,22 @@
|
|
75
87
|
}
|
76
88
|
}
|
77
89
|
|
78
|
-
export async function registerExtensions(loader: GLTFLoader, context: Context,
|
90
|
+
export async function registerExtensions(loader: GLTFLoader, context: Context, url: string) {
|
79
91
|
|
80
92
|
// Make sure to remove any url parameters from the sourceId (because the source id in the renderer does not have a ?v=xxx so it will not be able to register the resolved lightmap otherwise)
|
81
|
-
const idEnd =
|
82
|
-
if (idEnd >= 0)
|
93
|
+
const idEnd = url.indexOf("?");
|
94
|
+
if (idEnd >= 0) url = url.substring(0, idEnd);
|
83
95
|
|
84
|
-
|
85
96
|
loader.register(p => new NEEDLE_gameobject_data(p));
|
86
97
|
loader.register(p => new NEEDLE_persistent_assets(p));
|
87
|
-
loader.register(p => new NEEDLE_lightmaps(p, context.lightmaps,
|
88
|
-
loader.register(p => new NEEDLE_lighting_settings(p,
|
89
|
-
loader.register(p => new NEEDLE_techniques_webgl(p,
|
90
|
-
loader.register(p => new NEEDLE_render_objects(p,
|
91
|
-
loader.register(p => new NEEDLE_progressive(p,
|
98
|
+
loader.register(p => new NEEDLE_lightmaps(p, context.lightmaps, url));
|
99
|
+
loader.register(p => new NEEDLE_lighting_settings(p, url, context));
|
100
|
+
loader.register(p => new NEEDLE_techniques_webgl(p, url));
|
101
|
+
loader.register(p => new NEEDLE_render_objects(p, url));
|
102
|
+
loader.register(p => new NEEDLE_progressive(p, url));
|
92
103
|
loader.register(p => new EXT_texture_exr(p));
|
93
104
|
if (isResourceTrackingEnabled()) loader.register(p => new InternalUsageTrackerPlugin(p))
|
94
105
|
|
95
|
-
for (const plugin of _plugins) {
|
96
|
-
if (plugin.onImport) plugin.onImport(loader, sourceId, context);
|
97
|
-
}
|
98
|
-
|
99
106
|
await KHR_ANIMATIONPOINTER_IMPORT.catch(_ => { })
|
100
107
|
loader.register(p => {
|
101
108
|
if (GLTFAnimationPointerExtension) {
|
@@ -112,9 +119,18 @@
|
|
112
119
|
}
|
113
120
|
});
|
114
121
|
|
122
|
+
for (const plugin of _plugins) {
|
123
|
+
if (plugin.onImport) plugin.onImport(loader, url, context);
|
124
|
+
}
|
115
125
|
}
|
116
126
|
|
117
127
|
export function registerExportExtensions(exp: GLTFExporter, context: Context) {
|
118
128
|
for (const ext of _plugins)
|
119
129
|
if (ext.onExport) ext.onExport(exp, context);
|
130
|
+
}
|
131
|
+
|
132
|
+
/** @internal */
|
133
|
+
export function invokeAfterImportPluginHooks(url: string, gltf: GLTF, context: Context) {
|
134
|
+
for (const ext of _plugins)
|
135
|
+
if (ext.onLoaded) ext.onLoaded(url, gltf, context);
|
120
136
|
}
|
@@ -108,14 +108,15 @@
|
|
108
108
|
|
109
109
|
export function createMotion(name: string, id?: InstantiateIdProvider): Motion {
|
110
110
|
return {
|
111
|
-
name: "",
|
111
|
+
name: "Empty",
|
112
112
|
isLooping: false,
|
113
113
|
guid: id?.generateUUID() ?? MathUtils.generateUUID(),
|
114
114
|
index: -1,
|
115
|
-
clip: new AnimationClip(name,
|
115
|
+
clip: new AnimationClip(name, 0, []),
|
116
116
|
}
|
117
117
|
}
|
118
118
|
|
119
|
+
|
119
120
|
export declare type ClipMapping = {
|
120
121
|
/** the object this clip is for */
|
121
122
|
node: Object3D;
|
@@ -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";
|
@@ -220,7 +220,7 @@
|
|
220
220
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
221
221
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
222
222
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
223
|
-
|
223
|
+
|
224
224
|
// Register types
|
225
225
|
TypeStore.add("__Ignore", __Ignore);
|
226
226
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -53,6 +53,7 @@
|
|
53
53
|
/** When set to true the needle pwa plugin will not run */
|
54
54
|
noPWA?: boolean;
|
55
55
|
/** Pass in the VitePWA options */
|
56
|
+
/** @type {import("vite-plugin-pwa").VitePWAOptions} */
|
56
57
|
pwaOptions?: {};
|
57
58
|
|
58
59
|
/** used by nextjs config to forward the webpack module */
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Object3D, Quaternion, Vector3 } from "three";
|
1
|
+
import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { showBalloonWarning } from "../../engine/debug/index.js";
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
@@ -89,6 +89,15 @@
|
|
89
89
|
|
90
90
|
declare type WebXRImageTrackingEvent = (images: WebXRImageTrackingEvent[]) => void;
|
91
91
|
|
92
|
+
/** Initial state of assigned tracked image objects that are already present in the scene
|
93
|
+
* This is used to restore the state of the object when the webxr session has ended to return to the original state
|
94
|
+
*/
|
95
|
+
declare type InitialTrackedObjectState = {
|
96
|
+
visible: boolean;
|
97
|
+
parent: Object3D | undefined | null;
|
98
|
+
matrix: Matrix4;
|
99
|
+
}
|
100
|
+
|
92
101
|
export class WebXRImageTrackingModel {
|
93
102
|
|
94
103
|
@serializable(URL)
|
@@ -205,8 +214,15 @@
|
|
205
214
|
if (this.trackedImages) {
|
206
215
|
for (const trackedImage of this.trackedImages) {
|
207
216
|
if (trackedImage.object?.asset) {
|
208
|
-
|
209
|
-
|
217
|
+
// capture the initial state of tracked images in the scene to restore them when the session ends
|
218
|
+
const obj = trackedImage.object.asset as Object3D;
|
219
|
+
if (!obj.userData) obj.userData = {};
|
220
|
+
const state: InitialTrackedObjectState = {
|
221
|
+
visible: obj.visible,
|
222
|
+
parent: obj.parent,
|
223
|
+
matrix: obj.matrix.clone()
|
224
|
+
};
|
225
|
+
obj.userData["image-tracking"] = state;
|
210
226
|
}
|
211
227
|
}
|
212
228
|
}
|
@@ -216,6 +232,27 @@
|
|
216
232
|
}
|
217
233
|
};
|
218
234
|
|
235
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
236
|
+
if (this.trackedImages) {
|
237
|
+
for (const trackedImage of this.trackedImages) {
|
238
|
+
if (trackedImage.object?.asset) {
|
239
|
+
const obj = trackedImage.object.asset as Object3D;
|
240
|
+
if (obj.userData) {
|
241
|
+
// restore the initial state of tracked images in the scene
|
242
|
+
const state = obj.userData["image-tracking"] as InitialTrackedObjectState | undefined;
|
243
|
+
if (state) {
|
244
|
+
obj.visible = state.visible;
|
245
|
+
state.parent?.add(obj);
|
246
|
+
obj.matrix.copy(state.matrix);
|
247
|
+
obj.matrix.decompose(obj.position, obj.quaternion, obj.scale);
|
248
|
+
}
|
249
|
+
delete obj.userData["image-tracking"];
|
250
|
+
}
|
251
|
+
}
|
252
|
+
}
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
219
256
|
private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number, lastTrackingTime: number }>();
|
220
257
|
private readonly currentImages: WebXRTrackedImage[] = [];
|
221
258
|
|
@@ -234,6 +271,12 @@
|
|
234
271
|
showBalloonWarning(warning);
|
235
272
|
return;
|
236
273
|
}
|
274
|
+
// Check if enabled features (if available) contains image tracking - if it's not available this statement should not catch
|
275
|
+
// This handles mobile VR with image tracking. Seems like the "getImageTrackingResults" is available on the frame object but then we get runtime exceptions because the feature is (in VR) not enabled
|
276
|
+
else if (args.xr.session.enabledFeatures?.includes("image-tracking") === false) {
|
277
|
+
// Image tracking is not enabled for this session
|
278
|
+
return;
|
279
|
+
}
|
237
280
|
else if (frame.session && typeof frame.getImageTrackingResults === "function") {
|
238
281
|
const results = frame.getImageTrackingResults();
|
239
282
|
if (results.length > 0) {
|