@@ -37,6 +37,4 @@
|
|
37
37
|
}
|
38
38
|
}
|
39
39
|
|
40
|
-
console.log(config.resolve.alias);
|
41
|
-
|
42
40
|
}
|
@@ -7,7 +7,7 @@
|
|
7
7
|
if (needleConfig.codegenDirectory) {
|
8
8
|
const metaPath = workingDirectory + "/" + needleConfig.codegenDirectory + "/meta.json";
|
9
9
|
|
10
|
-
/**@type {import("../types").
|
10
|
+
/**@type {import("../types").needleMeta} */
|
11
11
|
const meta = require(metaPath);
|
12
12
|
return meta;
|
13
13
|
}
|
@@ -1,116 +0,0 @@
|
|
1
|
-
const { getMeta } = require("./config.cjs");
|
2
|
-
const https = require('https');
|
3
|
-
|
4
|
-
|
5
|
-
/**
|
6
|
-
* @param {string} code
|
7
|
-
* @param {string | null | undefined} licenseType
|
8
|
-
*/
|
9
|
-
module.exports.replaceLicense = async function (code, licenseType) {
|
10
|
-
|
11
|
-
if (!licenseType) {
|
12
|
-
const meta = getMeta();
|
13
|
-
if (meta) {
|
14
|
-
licenseType = meta.license;
|
15
|
-
}
|
16
|
-
}
|
17
|
-
|
18
|
-
if (typeof licenseType === "object") {
|
19
|
-
licenseType = await module.exports.resolveLicense(licenseType);
|
20
|
-
}
|
21
|
-
|
22
|
-
if (!licenseType) {
|
23
|
-
return code;
|
24
|
-
}
|
25
|
-
|
26
|
-
const index = code.indexOf("NEEDLE_ENGINE_LICENSE_TYPE");
|
27
|
-
if (index >= 0) {
|
28
|
-
const end = code.indexOf(";", index);
|
29
|
-
if (end >= 0) {
|
30
|
-
const line = code.substring(index, end);
|
31
|
-
const replaced = "NEEDLE_ENGINE_LICENSE_TYPE = \"" + licenseType + "\"";
|
32
|
-
code = code.replace(line, replaced);
|
33
|
-
return code;
|
34
|
-
}
|
35
|
-
}
|
36
|
-
}
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
const LICENSE_ENDPOINT = `https://urls.needle.tools/license-endpoint`
|
43
|
-
|
44
|
-
/**
|
45
|
-
* Resolve the license for a given license key (e.g. invoice id) and id (e.g. email that was used for the purchase)
|
46
|
-
* @param {import('../types/license.js').License} license
|
47
|
-
* @returns {Promise<string | null>}
|
48
|
-
*/
|
49
|
-
module.exports.resolveLicense = async function (license) {
|
50
|
-
if (typeof license !== "object") {
|
51
|
-
return license;
|
52
|
-
}
|
53
|
-
|
54
|
-
if (!license) {
|
55
|
-
return null;
|
56
|
-
}
|
57
|
-
|
58
|
-
if (!license.key) {
|
59
|
-
console.warn("WARN: License key is missing.");
|
60
|
-
return null;
|
61
|
-
}
|
62
|
-
else if (!license.id) {
|
63
|
-
console.warn("WARN: License id is missing.");
|
64
|
-
return null;
|
65
|
-
}
|
66
|
-
|
67
|
-
console.log("INFO: Resolve license for " + obscure(license.id + "::" + license.key));
|
68
|
-
const url = await fetch(LICENSE_ENDPOINT, { method: "GET" }).catch(console.error);
|
69
|
-
if (!url) {
|
70
|
-
console.warn("WARN: Failed to fetch license URL from endpoint");
|
71
|
-
return null;
|
72
|
-
}
|
73
|
-
const licenseRequestUrl = `${url}?email=${license.id}&key=${license.key}&version=2`;
|
74
|
-
const licenseResponse = await fetch(licenseRequestUrl, { method: "GET" }).catch(console.error);
|
75
|
-
if (!licenseResponse) {
|
76
|
-
console.warn("WARN: Failed to fetch license");
|
77
|
-
return null;
|
78
|
-
}
|
79
|
-
/** @type {{license:string}} */
|
80
|
-
const licenseJson = JSON.parse(licenseResponse);
|
81
|
-
console.log("\n");
|
82
|
-
if (licenseJson.license) {
|
83
|
-
console.log(`INFO: Successfully received \"${licenseJson.license?.toUpperCase()}\" license`)
|
84
|
-
return licenseJson.license;
|
85
|
-
}
|
86
|
-
console.warn("WARN: Received invalid license.");
|
87
|
-
return null;
|
88
|
-
}
|
89
|
-
|
90
|
-
|
91
|
-
/**
|
92
|
-
* @param {string} str
|
93
|
-
*/
|
94
|
-
function obscure(str) {
|
95
|
-
const start = str.substring(0, 3);
|
96
|
-
const end = str.substring(str.length - 3);
|
97
|
-
return start + "***" + end;
|
98
|
-
}
|
99
|
-
|
100
|
-
|
101
|
-
// NODE 16 doesn't support fetch yet
|
102
|
-
function fetch(url, options) {
|
103
|
-
return new Promise((resolve, reject) => {
|
104
|
-
https.get(url, options, (res) => {
|
105
|
-
let data = '';
|
106
|
-
res.on('data', (chunk) => {
|
107
|
-
data += chunk;
|
108
|
-
});
|
109
|
-
res.on('end', () => {
|
110
|
-
resolve(data);
|
111
|
-
});
|
112
|
-
}).on("error", (err) => {
|
113
|
-
reject(err);
|
114
|
-
});
|
115
|
-
});
|
116
|
-
}
|
@@ -1,5 +1,21 @@
|
|
1
|
-
const {
|
1
|
+
const { getMeta } = require("../common/config.cjs");
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
const mod = import("../common/license.js");
|
4
|
+
|
5
|
+
module.exports = async function (source, _map) {
|
6
|
+
|
7
|
+
/** @type {string | undefined} */
|
8
|
+
let team = undefined;
|
9
|
+
|
10
|
+
const meta = await getMeta();
|
11
|
+
if (meta) {
|
12
|
+
team = meta.license?.team;
|
13
|
+
}
|
14
|
+
|
15
|
+
// console.log("RESOLVE LICENSE...");
|
16
|
+
return (await mod).replaceLicense(source, {
|
17
|
+
team: team,
|
18
|
+
}).then(_ => {
|
19
|
+
// console.log("LICENSE RESOLVED");
|
20
|
+
})
|
5
21
|
};
|
@@ -7,6 +7,9 @@
|
|
7
7
|
* the first argument is the already resoled absolute path (it is only invoked if the path was found in node_modules)
|
8
8
|
* the 2,3,4 args are the same as in vite.alias (packageName, index, path);
|
9
9
|
*/
|
10
|
+
/**
|
11
|
+
* @type {Record<string, null | ((res: string, packageName: string, index: number, path: string) => string | null | void)>}
|
12
|
+
*/
|
10
13
|
const packages_to_resolve = {
|
11
14
|
// Handle all previous imports where users did import using @needle-engine/src
|
12
15
|
'@needle-tools/engine/src': (res, packageName, index, path) => {
|
@@ -15,6 +18,11 @@
|
|
15
18
|
return res + "/../lib";
|
16
19
|
}
|
17
20
|
},
|
21
|
+
/*
|
22
|
+
Removed the previously present @needle-tools/engine entry
|
23
|
+
because this is automatically done by vite according to whatever we define in our package.json exports
|
24
|
+
This did previously prevent us from declaring proper exports in package.json
|
25
|
+
*/
|
18
26
|
'@needle-tools/engine': (res, packageName, index, path) => {
|
19
27
|
// Check if the import is something like @needle-tools/engine/engine/engine_utils
|
20
28
|
// in which case we want to resolve into the lib directory
|
@@ -22,7 +30,9 @@
|
|
22
30
|
return res + "/lib";
|
23
31
|
}
|
24
32
|
},
|
25
|
-
|
33
|
+
|
34
|
+
/*
|
35
|
+
Removed. Three.js is manually resolved below to ensure all dependencies resolve to the same three.js version.
|
26
36
|
'three': null,
|
27
37
|
*/
|
28
38
|
'peerjs': null,
|
@@ -53,8 +63,8 @@
|
|
53
63
|
outputDebugFile.write(`[needle-alias] Logging to: ${outputFilePath} (${timestamp})\n`);
|
54
64
|
log("[needle-alias] Logging to: ", outputFilePath);
|
55
65
|
}
|
56
|
-
|
57
66
|
|
67
|
+
|
58
68
|
const aliasPlugin = {
|
59
69
|
name: "needle-alias",
|
60
70
|
config(config) {
|
@@ -134,19 +144,27 @@
|
|
134
144
|
function addPathResolver(name, aliasDict, cb) {
|
135
145
|
// If a package at the node_modules path exist we resolve the request there
|
136
146
|
// introduced in 89a50718c38940abb99ee16c5e029065e41d7d65
|
137
|
-
const
|
147
|
+
const fullpath = path.resolve(projectDir, 'node_modules', name);
|
138
148
|
if (typeof cb !== "function") cb = null;
|
139
149
|
const isDevEnvironment = process.env.NODE_ENV === "development";
|
140
|
-
|
150
|
+
|
151
|
+
if (existsSync(fullpath)) {
|
141
152
|
aliasDict[name] = (packageName, index, path) => {
|
142
153
|
if (cb !== null && !isDevEnvironment) {
|
143
|
-
const overrideResult = cb(
|
154
|
+
const overrideResult = cb(fullpath, packageName, index, path);
|
144
155
|
if (overrideResult !== undefined)
|
145
156
|
if (existsSync(overrideResult)) {
|
157
|
+
console.warn(`[needle-alias] \"${path}\" was requested and resolved to \"${overrideResult}\"`);
|
146
158
|
return overrideResult;
|
147
159
|
}
|
148
160
|
}
|
149
|
-
|
161
|
+
|
162
|
+
if (path != packageName) {
|
163
|
+
// TODO: we might want to check if the package.json exports contains the path to see if it's valid
|
164
|
+
console.warn(`[needle-alias] \"${path}\" was requested and resolved to \"${fullpath}\"`);
|
165
|
+
}
|
166
|
+
|
167
|
+
return fullpath;
|
150
168
|
}
|
151
169
|
}
|
152
170
|
}
|
@@ -5,6 +5,7 @@
|
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Injects needle asap script into the index.html for when the main needle engine bundle is still being downloaded
|
8
|
+
* @param {"build" | "serve"} command
|
8
9
|
* @param {import('../types').userSettings} userSettings
|
9
10
|
* @returns {Promise<import('vite').Plugin | null>}
|
10
11
|
*/
|
@@ -14,6 +15,10 @@
|
|
14
15
|
|
15
16
|
fixMainTs();
|
16
17
|
|
18
|
+
if (command != "build") {
|
19
|
+
return null;
|
20
|
+
}
|
21
|
+
|
17
22
|
return {
|
18
23
|
name: 'needle:asap',
|
19
24
|
transformIndexHtml: {
|
@@ -1,21 +1,85 @@
|
|
1
|
-
import { ChildProcess, exec
|
2
|
-
import { getOutputDirectory, loadConfig
|
3
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
1
|
+
import { ChildProcess, exec } from 'child_process';
|
2
|
+
import { getOutputDirectory, loadConfig } from './config.js';
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
4
|
+
import { copyFilesSync } from '../common/files.js';
|
5
|
+
import { delay } from '../common/timers.js';
|
4
6
|
|
7
|
+
/**
|
8
|
+
* @param {import('../types').userSettings} config
|
9
|
+
* @returns {boolean}
|
10
|
+
*/
|
11
|
+
function validateCloudBuildConfiguration(config) {
|
12
|
+
if (config.buildPipeline != undefined) {
|
13
|
+
if (process.env.CI) {
|
14
|
+
if ((process.env.NEEDLE_CLOUD_TOKEN == undefined || process.env.NEEDLE_CLOUD_TOKEN.length <= 0)) {
|
15
|
+
const isGithubAction = process.env.CI && process.env.GITHUB_ACTION;
|
16
|
+
let msg = `Missing Needle Cloud access token. Please set your Needle Cloud token via \`process.env.NEEDLE_CLOUD_TOKEN\`.`;
|
17
|
+
if (isGithubAction) {
|
18
|
+
msg += `
|
19
|
+
Make sure to pass the token as a secret to the Github action.
|
20
|
+
For this you may have to modify your .github workflow yaml file to include the following code:
|
21
|
+
env:
|
22
|
+
- NEEDLE_CLOUD_TOKEN: \${{ secrets.NEEDLE_CLOUD_TOKEN }}
|
23
|
+
`;
|
24
|
+
let error = `${msg}`;
|
25
|
+
throw new Error(error);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
return true;
|
32
|
+
}
|
33
|
+
|
5
34
|
// see https://linear.app/needle/issue/NE-3798
|
6
35
|
|
7
|
-
export let buildPipelineTask;
|
8
36
|
|
37
|
+
/** @type {Promise<any>|null} */
|
38
|
+
let buildPipelineTask;
|
39
|
+
/** @type {null | {tempDirectory:string, outputDirectory:string}} */
|
40
|
+
let buildPipelineTaskResults = null;
|
41
|
+
|
42
|
+
export function waitForBuildPipelineToFinish() {
|
43
|
+
return buildPipelineTask;
|
44
|
+
}
|
45
|
+
|
46
|
+
/** This time is set when a build plugin is triggered to run
|
47
|
+
* We increase the time by 10-20 seconds each time because we might have a multi step process
|
48
|
+
* where the build pipeline is triggered in the SSR build (which we can not always detect)
|
49
|
+
* and the final client build is triggered later (but the build pipeline is still running and waiting)
|
50
|
+
*/
|
51
|
+
let maxOutputDirectoryCreatedWaitTime = 0;
|
9
52
|
/**
|
53
|
+
* @param {boolean} debugLog
|
54
|
+
*/
|
55
|
+
function increaseMaxWaitTime(debugLog) {
|
56
|
+
maxOutputDirectoryCreatedWaitTime = Date.now();
|
57
|
+
let maxWaitTime = 10_000;
|
58
|
+
if (process.env.CI) {
|
59
|
+
maxWaitTime += 50_000;
|
60
|
+
}
|
61
|
+
maxOutputDirectoryCreatedWaitTime += maxWaitTime;
|
62
|
+
if (debugLog) {
|
63
|
+
log(`Increased max wait time by ${maxWaitTime / 1000}sec until ${new Date(maxOutputDirectoryCreatedWaitTime).toISOString()}`);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
10
68
|
* Runs the needle build pipeline as part of the vite build process
|
69
|
+
* @param {string} command
|
70
|
+
* @param {import('vite').UserConfig} config
|
11
71
|
* @param {import('../types').userSettings} userSettings
|
12
|
-
* @returns {import('vite').Plugin}
|
72
|
+
* @returns {Promise<import('vite').Plugin | null>}
|
13
73
|
*/
|
14
74
|
export const needleBuildPipeline = async (command, config, userSettings) => {
|
15
75
|
|
16
76
|
// we only want to run compression here if this is a distribution build
|
17
77
|
// this is handled however in the `apply` hook
|
18
|
-
if (userSettings.noBuildPipeline) return;
|
78
|
+
if (userSettings.noBuildPipeline) return null;
|
79
|
+
if (userSettings.buildPipeline?.enabled === false) {
|
80
|
+
log("Skipping build pipeline because it is disabled in the user settings via `buildPipeline.enabled: false`");
|
81
|
+
return null;
|
82
|
+
}
|
19
83
|
|
20
84
|
const packageJsonPath = process.cwd() + "/package.json";
|
21
85
|
await fixPackageJson(packageJsonPath);
|
@@ -26,53 +90,102 @@
|
|
26
90
|
if (productionArgument >= 0) {
|
27
91
|
shouldRun = true;
|
28
92
|
}
|
29
|
-
else {
|
30
|
-
const meta = await loadConfig();
|
31
|
-
if (meta && meta.developmentBuild === false) {
|
32
|
-
shouldRun = true;
|
33
|
-
}
|
34
|
-
}
|
35
93
|
|
36
94
|
if (!shouldRun) {
|
37
95
|
log("Skipping build pipeline because this is a development build.\n- Invoke with `--production` to run the build pipeline.\n- For example \"vite build -- --production\".");
|
38
96
|
await new Promise((resolve, _) => setTimeout(resolve, 1000));
|
39
|
-
return;
|
97
|
+
return null;
|
40
98
|
}
|
41
99
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
100
|
+
if (process.env.CI) {
|
101
|
+
log("Running in CI environment");
|
102
|
+
}
|
103
|
+
|
104
|
+
validateCloudBuildConfiguration(userSettings);
|
105
|
+
|
106
|
+
const verboseOutput = userSettings?.buildPipeline?.verbose || false;
|
107
|
+
let taskHasCompleted = false;
|
108
|
+
|
46
109
|
return {
|
47
110
|
name: 'needle:buildpipeline',
|
48
111
|
enforce: "post",
|
49
|
-
apply:
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
112
|
+
apply: (_conf, env) => {
|
113
|
+
if (verboseOutput) {
|
114
|
+
log("Apply:", env);
|
115
|
+
}
|
116
|
+
// Don't run for SSR builds (e.g. sveltekit).
|
117
|
+
// Unfortunately this is always falls in vite 4.3 so we can not rely on it solely
|
118
|
+
if (env.ssrBuild) return false;
|
119
|
+
// Dont run if there's already a build pipeline task running
|
120
|
+
if (env.command === "build") {
|
121
|
+
increaseMaxWaitTime(verboseOutput);
|
122
|
+
if (buildPipelineTask) {
|
123
|
+
return false;
|
124
|
+
}
|
125
|
+
return true;
|
126
|
+
}
|
127
|
+
return false;
|
57
128
|
},
|
129
|
+
buildEnd(error) {
|
130
|
+
increaseMaxWaitTime(verboseOutput);
|
131
|
+
|
132
|
+
if (verboseOutput) {
|
133
|
+
log("Build end:", error ?? "No error");
|
134
|
+
}
|
135
|
+
if (error) {
|
136
|
+
// if there was an error during the build we should not run the build pipeline
|
137
|
+
}
|
138
|
+
else {
|
139
|
+
if (buildPipelineTask) {
|
140
|
+
log("Build pipeline already running...");
|
141
|
+
return;
|
142
|
+
}
|
143
|
+
let taskSucceeded = false;
|
144
|
+
// start the compression process once vite is done copying the files
|
145
|
+
buildPipelineTask = invokeBuildPipeline(userSettings)
|
146
|
+
.then((res) => {
|
147
|
+
taskSucceeded = res;
|
148
|
+
})
|
149
|
+
.finally(() => {
|
150
|
+
taskHasCompleted = true;
|
151
|
+
if (!taskSucceeded) {
|
152
|
+
throw new Error("[needle-buildpipeline] - Build pipeline failed. Check the logs for more information.");
|
153
|
+
}
|
154
|
+
else {
|
155
|
+
log("Finished successfully");
|
156
|
+
}
|
157
|
+
});
|
158
|
+
}
|
159
|
+
},
|
58
160
|
closeBundle() {
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
161
|
+
if (!buildPipelineTask) {
|
162
|
+
return;
|
163
|
+
}
|
164
|
+
if (!taskHasCompleted) {
|
165
|
+
log("Waiting for build pipeline to finish...");
|
166
|
+
}
|
167
|
+
// // this is the last hook that is called, so we can wait for the task to finish here
|
168
|
+
return buildPipelineTask = buildPipelineTask?.then(() => {
|
169
|
+
// Copy the results to their final output directory.
|
170
|
+
if (buildPipelineTaskResults != null) {
|
171
|
+
log("Copying files from temporary output directory to final output directory...");
|
172
|
+
const ctx = { count: 0 }
|
173
|
+
copyFilesSync(buildPipelineTaskResults.tempDirectory, buildPipelineTaskResults.outputDirectory, true, ctx);
|
174
|
+
log(`Copied ${ctx.count} file(s)`);
|
175
|
+
}
|
176
|
+
else {
|
177
|
+
log("No files to copy - build pipeline did not run or did not finish successfully");
|
178
|
+
}
|
65
179
|
});
|
66
|
-
return task.then(_ => {
|
67
|
-
log("finished", taskSucceeded ? "successfully" : "with errors");
|
68
|
-
});
|
69
180
|
},
|
70
181
|
}
|
71
182
|
}
|
72
183
|
|
184
|
+
|
73
185
|
/**
|
74
186
|
* Previously we did always install the build pipeline and run an extra command to invoke the build pipeline.
|
75
187
|
* This is now done automatically by the needle build pipeline plugin - so we update all legacy projects to use the new method.
|
188
|
+
* @param {string} packageJsonPath
|
76
189
|
*/
|
77
190
|
async function fixPackageJson(packageJsonPath) {
|
78
191
|
if (!existsSync(packageJsonPath)) {
|
@@ -89,9 +202,11 @@
|
|
89
202
|
writeFileSync(packageJsonPath, fixed);
|
90
203
|
}
|
91
204
|
|
205
|
+
/** @param {any} args */
|
92
206
|
function log(...args) {
|
93
207
|
console.log("[needle-buildpipeline]", ...args);
|
94
208
|
}
|
209
|
+
/** @param {any} args */
|
95
210
|
function warn(...args) {
|
96
211
|
console.warn("WARN: [needle-buildpipeline]", ...args);
|
97
212
|
}
|
@@ -104,63 +219,135 @@
|
|
104
219
|
const installPath = "node_modules/@needle-tools/gltf-build-pipeline";
|
105
220
|
const fullInstallPath = process.cwd() + "/" + installPath;
|
106
221
|
const existsLocally = existsSync(fullInstallPath);
|
107
|
-
if (
|
108
|
-
|
109
|
-
// throw new Error(`ERR: Build pipeline not found at \"${fullInstallPath}\". \nTo disable this plugin you can pass \"noBuildPipeline: true\" to the needle config in vite.config.js`);
|
110
|
-
warn("@needle-tools/gltf-build-pipeline not found in package - using latest version")
|
222
|
+
if (existsLocally) {
|
223
|
+
log("Found local build pipeline installation at " + fullInstallPath);
|
111
224
|
}
|
112
|
-
await
|
225
|
+
await delay(500);
|
113
226
|
const outputDirectory = getOutputDirectory() + "/assets";
|
114
|
-
|
227
|
+
const startWaitTime = Date.now();
|
228
|
+
const maxEndTime = startWaitTime + 120_000;
|
229
|
+
/** wait until the output directory exists
|
230
|
+
* @param {number} iteration
|
231
|
+
* @returns {Promise<boolean>}
|
232
|
+
*/
|
115
233
|
function waitForOutputDirectory(iteration) {
|
234
|
+
// we wait for the output directory
|
116
235
|
if (!existsSync(outputDirectory)) {
|
117
|
-
if (
|
236
|
+
if (maxOutputDirectoryCreatedWaitTime != 0 && Date.now() > maxOutputDirectoryCreatedWaitTime) {
|
118
237
|
return Promise.resolve(false);
|
119
238
|
}
|
120
|
-
if (
|
121
|
-
|
239
|
+
else if (Date.now() > maxEndTime) {
|
240
|
+
log("Max wait time exceeded - aborting...");
|
241
|
+
return Promise.resolve(false);
|
242
|
+
}
|
243
|
+
if (iteration <= 0) log(`Waiting for output directory to be created... (${outputDirectory})`);
|
244
|
+
return delay(1000).then(() => waitForOutputDirectory(iteration + 1));
|
122
245
|
}
|
123
246
|
return Promise.resolve(true);
|
124
247
|
}
|
125
248
|
if (!await waitForOutputDirectory(0)) {
|
126
|
-
warn(
|
127
|
-
return;
|
249
|
+
warn(`Output directory not found/created at \"${outputDirectory}\" - aborting...`);
|
250
|
+
return false;
|
128
251
|
}
|
129
|
-
const files = readdirSync(outputDirectory).filter(f => f.endsWith(".glb") || f.endsWith(".gltf"));
|
130
|
-
log(files.length
|
252
|
+
const files = readdirSync(outputDirectory).filter(f => f.endsWith(".glb") || f.endsWith(".gltf") || f.endsWith(".vrm") || f.endsWith(".fbx"));
|
253
|
+
log(`${files.length} file(s) to process in ${outputDirectory}`);
|
131
254
|
|
132
255
|
/** @type {null | ChildProcess} */
|
133
|
-
let
|
256
|
+
let proc = null;
|
134
257
|
|
135
|
-
|
136
|
-
|
258
|
+
let cloudAccessToken = opts.license?.accessToken;
|
259
|
+
if (!cloudAccessToken) {
|
260
|
+
cloudAccessToken = process.env.NEEDLE_CLOUD_TOKEN;
|
261
|
+
}
|
262
|
+
const runInCloud = typeof cloudAccessToken === "string" && cloudAccessToken.length > 0;
|
263
|
+
// if a user has defined the build pipeline settings object but not passed in a token we should print out some information
|
264
|
+
// or perhaps log an error / prevent the build from running completely
|
265
|
+
if (opts.buildPipeline && !runInCloud && process.env.CI) {
|
266
|
+
warn(`No cloud access token found. Please set it via process.env.NEEDLE_CLOUD_TOKEN`);
|
267
|
+
return false;
|
268
|
+
}
|
269
|
+
|
270
|
+
|
271
|
+
// put the processed files first in a temporary directory. They will be moved to the output directory at the end of the buildstep
|
272
|
+
// this is so that processes like sveltekit-static-adapter can run first and does not override already compressed files
|
273
|
+
const tempOutputPath = process.cwd() + "/node_modules/.needle/build-pipeline/output";
|
274
|
+
if (existsSync(tempOutputPath)) {
|
275
|
+
log("Removing temporary output directory at " + tempOutputPath);
|
276
|
+
rmSync(tempOutputPath, { recursive: true, force: true });
|
277
|
+
}
|
278
|
+
mkdirSync(tempOutputPath, { recursive: true });
|
279
|
+
|
280
|
+
/** @param {number} code */
|
281
|
+
function onExit(code) {
|
282
|
+
if (code === 0)
|
283
|
+
buildPipelineTaskResults = {
|
284
|
+
tempDirectory: tempOutputPath,
|
285
|
+
outputDirectory: outputDirectory
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
// allow running the build pipeline in the cloud. It requires and access token to be set in the vite.config.js
|
290
|
+
// this can be set via e.g. process.env.NEEDLE_CLOUD_TOKEN
|
291
|
+
if (runInCloud) {
|
292
|
+
if (!cloudAccessToken || !(typeof cloudAccessToken === "string") || cloudAccessToken.length <= 0) {
|
293
|
+
throw new Error("No cloud access token configured. Please set it via process.env.NEEDLE_CLOUD_TOKEN or in the vite.config.js");
|
294
|
+
}
|
295
|
+
let cmd = `npx --yes needle-cloud@main optimize "${outputDirectory}" --token ${cloudAccessToken}`;
|
296
|
+
let projectName = opts.buildPipeline?.projectName;
|
297
|
+
// Default project name for compression
|
298
|
+
// TODO: maybe this should be taken from the package.json name field or needle.config?
|
299
|
+
if (!projectName) {
|
300
|
+
projectName = "compression";
|
301
|
+
}
|
302
|
+
if (projectName) {
|
303
|
+
cmd += ` --name "${projectName}"`;
|
304
|
+
}
|
305
|
+
if (opts.buildPipeline?.verbose === true) {
|
306
|
+
cmd += " --verbose";
|
307
|
+
}
|
308
|
+
cmd += " --outdir \"" + tempOutputPath + "\"";
|
309
|
+
console.log("\n")
|
310
|
+
log("Running compression in cloud â›…");
|
311
|
+
proc = exec(cmd);
|
312
|
+
}
|
313
|
+
else if (existsLocally) {
|
314
|
+
const cmd = `needle-gltf transform "${outputDirectory}" \"${tempOutputPath}\"`;
|
137
315
|
log("Running command \"" + cmd + "\" at " + process.cwd() + "...");
|
138
|
-
|
316
|
+
proc = exec(cmd, { cwd: installPath });
|
139
317
|
}
|
140
318
|
else {
|
141
|
-
const version = opts.
|
142
|
-
const cmd = `npx --yes @needle-tools/gltf-build-pipeline@${version} transform "${outputDirectory}"`;
|
143
|
-
log(
|
144
|
-
|
319
|
+
const version = opts.buildPipeline?.version || "latest";
|
320
|
+
const cmd = `npx --yes @needle-tools/gltf-build-pipeline@${version} transform "${outputDirectory}" \"${tempOutputPath}\"`;
|
321
|
+
log(`Running compression locally with version ${version}`);
|
322
|
+
proc = exec(cmd);
|
145
323
|
}
|
146
|
-
|
324
|
+
/** @param {any} data */
|
325
|
+
function onLog(data) {
|
147
326
|
if (data.length <= 0) return;
|
148
327
|
// ensure that it doesnt end with a newline
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
328
|
+
while (data.endsWith("\n")) data = data.slice(0, -1);
|
329
|
+
if (typeof data === "string") {
|
330
|
+
if (data.startsWith("ERR:")) {
|
331
|
+
console.error(data);
|
332
|
+
return;
|
333
|
+
}
|
334
|
+
else if (data.startsWith("WARN:")) {
|
335
|
+
console.warn(data);
|
336
|
+
return;
|
337
|
+
}
|
338
|
+
// Ignore empty lines
|
339
|
+
else if (data.trim().length <= 0) {
|
340
|
+
return;
|
341
|
+
}
|
342
|
+
}
|
343
|
+
log(data);
|
344
|
+
}
|
345
|
+
proc.stdout?.on('data', onLog);
|
346
|
+
proc.stderr?.on('data', onLog);
|
153
347
|
return new Promise((resolve, reject) => {
|
154
|
-
|
348
|
+
proc.on('exit', (code) => {
|
349
|
+
onExit(code || 0);
|
155
350
|
resolve(code === 0);
|
156
351
|
});
|
157
352
|
});
|
158
353
|
}
|
159
|
-
|
160
|
-
function wait(ms) {
|
161
|
-
return new Promise((resolve, reject) => {
|
162
|
-
setTimeout(() => {
|
163
|
-
resolve();
|
164
|
-
}, ms);
|
165
|
-
});
|
166
|
-
}
|
@@ -16,11 +16,11 @@
|
|
16
16
|
totalsize: 0,
|
17
17
|
files: []
|
18
18
|
};
|
19
|
-
console.log(
|
19
|
+
console.log(`[needle-buildinfo] - Begin collecting files in \"${buildDirectory}\"`);
|
20
20
|
recursivelyCollectFiles(buildDirectory, "", buildInfo);
|
21
21
|
const buildInfoPath = `${buildDirectory}/needle.buildinfo.json`;
|
22
22
|
const totalSizeInMB = buildInfo.totalsize / 1024 / 1024;
|
23
|
-
console.log(`[needle-buildinfo] - Collected ${buildInfo.files.length} files (${totalSizeInMB.toFixed(2)} MB).
|
23
|
+
console.log(`[needle-buildinfo] - Collected ${buildInfo.files.length} files (${totalSizeInMB.toFixed(2)} MB). Writing build info to \"${buildInfoPath}\"`);
|
24
24
|
fs.writeFileSync(buildInfoPath, JSON.stringify(buildInfo));
|
25
25
|
}
|
26
26
|
|
@@ -39,10 +39,11 @@
|
|
39
39
|
}
|
40
40
|
const newPath = path?.length <= 0 ? entry.name : `${path}/${entry.name}`;
|
41
41
|
const newDirectory = `${directory}/${entry.name}`;
|
42
|
-
console.log(
|
42
|
+
console.log(`[needle-buildinfo] - Collect files in \"/${newPath}\"`);
|
43
43
|
recursivelyCollectFiles(newDirectory, newPath, info);
|
44
44
|
} else {
|
45
45
|
const relpath = `${path}/${entry.name}`;
|
46
|
+
// console.log("[needle-buildinfo] - New File: " + relpath);
|
46
47
|
const filehash = crypto.createHash('sha256');
|
47
48
|
const fullpath = `${directory}/${entry.name}`;
|
48
49
|
filehash.update(fs.readFileSync(fullpath));
|
@@ -1,32 +1,38 @@
|
|
1
1
|
import { createBuildInfoFile } from '../common/buildinfo.js';
|
2
|
-
import {
|
2
|
+
import { waitForBuildPipelineToFinish } from './build-pipeline.js';
|
3
3
|
import { getOutputDirectory } from './config.js';
|
4
4
|
|
5
|
+
let level = 0;
|
5
6
|
|
6
|
-
|
7
7
|
/** Create a buildinfo file in the build directory
|
8
8
|
* @param {import('../types').userSettings} userSettings
|
9
|
-
* @returns {import('vite').Plugin}
|
9
|
+
* @returns {import('vite').Plugin | null}
|
10
10
|
*/
|
11
11
|
export const needleBuildInfo = (command, config, userSettings) => {
|
12
12
|
|
13
|
-
if (userSettings?.noBuildInfo) return;
|
13
|
+
if (userSettings?.noBuildInfo) return null;
|
14
14
|
|
15
15
|
return {
|
16
16
|
name: 'needle:buildinfo',
|
17
17
|
apply: "build",
|
18
18
|
enforce: "post",
|
19
|
+
buildStart: () => {
|
20
|
+
level++;
|
21
|
+
},
|
19
22
|
closeBundle: async () => {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
if (--level > 0) {
|
24
|
+
console.log("[needle-buildinfo] - Skipped because of nested build");
|
25
|
+
return;
|
26
|
+
}
|
27
|
+
const task = waitForBuildPipelineToFinish();
|
28
|
+
if (task instanceof Promise) {
|
29
|
+
console.log("[needle-buildinfo] - Waiting for build pipeline to finish");
|
30
|
+
await task.catch(() => { }).finally(() => console.log("[needle-buildinfo] - Build pipeline finished!"));
|
31
|
+
}
|
32
|
+
// wait for gzip
|
33
|
+
await delay(500);
|
34
|
+
const buildDirectory = getOutputDirectory();
|
35
|
+
createBuildInfoFile(buildDirectory);
|
30
36
|
}
|
31
37
|
}
|
32
38
|
}
|
@@ -4,7 +4,8 @@
|
|
4
4
|
let didLogCanNotFindConfig = false;
|
5
5
|
|
6
6
|
/** the codegen meta file
|
7
|
-
* @
|
7
|
+
* @param {string | null} path
|
8
|
+
* @returns {Promise<import("../types/needleConfig").needleMeta | null>}
|
8
9
|
*/
|
9
10
|
export async function loadConfig(path) {
|
10
11
|
try {
|
@@ -32,12 +33,12 @@
|
|
32
33
|
console.error("Could not find config file at " + path);
|
33
34
|
}
|
34
35
|
}
|
35
|
-
return
|
36
|
+
return null;
|
36
37
|
}
|
37
38
|
catch (err) {
|
38
|
-
console.error("Error loading config file");
|
39
|
+
console.error("ERR: Error loading config file");
|
39
40
|
console.error(err);
|
40
|
-
return
|
41
|
+
return null;
|
41
42
|
}
|
42
43
|
}
|
43
44
|
|
@@ -1,41 +1,52 @@
|
|
1
1
|
|
2
2
|
import { resolve, join, isAbsolute } from 'path'
|
3
|
-
import { existsSync, statSync, mkdirSync, readdirSync, copyFileSync, mkdir } from 'fs';
|
3
|
+
import { existsSync, statSync, mkdirSync, readdirSync, copyFileSync, mkdir, rmSync } from 'fs';
|
4
4
|
import { builtAssetsDirectory, tryLoadProjectConfig } from './config.js';
|
5
|
+
import { copyFilesSync } from '../common/files.js';
|
5
6
|
|
7
|
+
const pluginName = "needle-copy-files";
|
6
8
|
|
9
|
+
|
7
10
|
/** copy files on build from assets to dist
|
8
11
|
* @param {import('../types').userSettings} userSettings
|
12
|
+
* @returns {import('vite').Plugin | null}
|
9
13
|
*/
|
10
14
|
export const needleCopyFiles = (command, config, userSettings) => {
|
11
15
|
|
12
16
|
if (config?.noCopy === true || userSettings?.noCopy === true) {
|
13
|
-
return;
|
17
|
+
return null;
|
14
18
|
}
|
15
19
|
|
16
20
|
return {
|
17
21
|
name: 'needle-copy-files',
|
22
|
+
// explicitly don't enforce post or pre because we want to run before the build-pipeline plugin
|
23
|
+
enforce: "pre",
|
18
24
|
buildStart() {
|
19
|
-
return run(
|
25
|
+
return run("start", config);
|
20
26
|
},
|
21
27
|
closeBundle() {
|
22
|
-
return run(
|
28
|
+
return run("end", config);
|
23
29
|
},
|
24
30
|
}
|
25
31
|
}
|
26
32
|
|
27
|
-
|
33
|
+
/**
|
34
|
+
* @param {"start" | "end"} buildstep
|
35
|
+
* @param {import('../types').userSettings} config
|
36
|
+
*/
|
37
|
+
async function run(buildstep, config) {
|
38
|
+
console.log(`[${pluginName}] - Copy files at ${buildstep}`);
|
28
39
|
const copyIncludesFromEngine = config?.copyIncludesFromEngine ?? true;
|
29
40
|
|
30
41
|
const baseDir = process.cwd();
|
31
|
-
const
|
42
|
+
const override = buildstep === "start";
|
32
43
|
|
33
44
|
let assetsDirName = "assets";
|
34
45
|
let outdirName = "dist";
|
35
46
|
|
36
47
|
const needleConfig = tryLoadProjectConfig();
|
37
48
|
if (needleConfig) {
|
38
|
-
assetsDirName = needleConfig.assetsDirectory;
|
49
|
+
assetsDirName = needleConfig.assetsDirectory || assetsDirName;
|
39
50
|
while (assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);
|
40
51
|
|
41
52
|
if (needleConfig.buildDirectory)
|
@@ -48,60 +59,67 @@
|
|
48
59
|
if (existsSync(engineIncludeDir)) {
|
49
60
|
console.log(`[${pluginName}] - Copy engine include to ${baseDir}/include`)
|
50
61
|
const projectIncludeDir = resolve(baseDir, 'include');
|
51
|
-
|
62
|
+
copyFilesSync(engineIncludeDir, projectIncludeDir);
|
52
63
|
}
|
53
64
|
}
|
54
65
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
}
|
66
|
+
const outDir = resolve(baseDir, outdirName);
|
67
|
+
if (!existsSync(outDir)) {
|
68
|
+
mkdirSync(outDir);
|
69
|
+
}
|
60
70
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
}
|
75
|
-
const src = resolve(baseDir, entry);
|
76
|
-
const dest = resolvePath(outDir, entry);
|
77
|
-
if (existsSync(src)) {
|
78
|
-
console.log(`[${pluginName}] - Copy ${entry} to ${outdirName}/${entry}`)
|
79
|
-
copyRecursiveSync(src, dest);
|
80
|
-
}
|
71
|
+
// copy a list of files or directories declared in build.copy = [] in the needle.config.json
|
72
|
+
/*
|
73
|
+
"build": {
|
74
|
+
"copy": ["myFolder", "myFile.txt"]
|
75
|
+
}
|
76
|
+
*/
|
77
|
+
if (needleConfig?.build?.copy) {
|
78
|
+
const arr = needleConfig.build.copy;
|
79
|
+
for (let i = 0; i < arr.length; i++) {
|
80
|
+
const entry = arr[i];
|
81
|
+
if (Array.isArray(entry)) {
|
82
|
+
console.log("WARN: build.copy can only contain string paths to copy to. Found array instead.");
|
83
|
+
continue;
|
81
84
|
}
|
85
|
+
const src = resolve(baseDir, entry);
|
86
|
+
const dest = resolvePath(outDir, entry);
|
87
|
+
if (existsSync(src) && dest) {
|
88
|
+
console.log(`[${pluginName}] - Copy ${entry} to ${outdirName}/${entry}`)
|
89
|
+
copyFilesSync(src, dest, override);
|
90
|
+
}
|
82
91
|
}
|
92
|
+
}
|
83
93
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
94
|
+
// copy assets dir
|
95
|
+
const assetsDir = resolve(baseDir, assetsDirName);
|
96
|
+
if (existsSync(assetsDir)) {
|
97
|
+
const targetDir = resolve(outDir, 'assets');
|
98
|
+
// ensure that the target directory exists and is cleared if it already exists
|
99
|
+
// otherwise we might run into issues where the build pipeline is running for already compressed files
|
100
|
+
if (override && existsSync(targetDir)) {
|
101
|
+
console.log(`[${pluginName}] - Clearing target directory \"${targetDir}\"`);
|
102
|
+
rmSync(targetDir, { recursive: true, force: true });
|
90
103
|
}
|
91
|
-
|
92
|
-
|
93
|
-
const includeDir = resolve(baseDir, 'include');
|
94
|
-
if (existsSync(includeDir)) {
|
95
|
-
console.log(`[${pluginName}] - Copy include to ${outdirName}/include`)
|
96
|
-
const targetDir = resolve(outDir, 'include');
|
97
|
-
copyRecursiveSync(includeDir, targetDir);
|
98
|
-
}
|
104
|
+
console.log(`[${pluginName}] - Copy assets to ${outdirName}/${builtAssetsDirectory()}`)
|
105
|
+
copyFilesSync(assetsDir, targetDir, override);
|
99
106
|
}
|
107
|
+
else console.log(`WARN: No assets directory found. Skipping copy of ${assetsDirName} resolved to ${assetsDir}`)
|
108
|
+
|
109
|
+
// copy include dir
|
110
|
+
const includeDir = resolve(baseDir, 'include');
|
111
|
+
if (existsSync(includeDir)) {
|
112
|
+
console.log(`[${pluginName}] - Copy include to ${outdirName}/include`)
|
113
|
+
const targetDir = resolve(outDir, 'include');
|
114
|
+
copyFilesSync(includeDir, targetDir, override);
|
115
|
+
}
|
100
116
|
}
|
101
117
|
|
102
118
|
/** resolves relative or absolute paths to a path inside the out directory
|
103
119
|
* for example D:/myFile.txt would resolve to outDir/myFile.txt
|
104
120
|
* wherereas "some/relative/path" would become outDir/some/relative/path
|
121
|
+
* @param {string} outDir
|
122
|
+
* @param {string} pathValue
|
105
123
|
*/
|
106
124
|
function resolvePath(outDir, pathValue) {
|
107
125
|
if (isAbsolute(pathValue)) {
|
@@ -110,29 +128,12 @@
|
|
110
128
|
var stats = exists && statSync(pathValue);
|
111
129
|
if (stats.isDirectory()) {
|
112
130
|
const dirName = pathValue.replaceAll('\\', '/').split('/').pop();
|
131
|
+
if (!dirName) return null;
|
113
132
|
return resolve(outDir, dirName);
|
114
133
|
}
|
115
134
|
const fileName = pathValue.replaceAll('\\', '/').split('/').pop();
|
135
|
+
if (!fileName) return null;
|
116
136
|
return resolve(outDir, fileName);
|
117
137
|
}
|
118
138
|
return resolve(outDir, pathValue);
|
119
|
-
}
|
120
|
-
|
121
|
-
function copyRecursiveSync(src, dest) {
|
122
|
-
if (dest === null) {
|
123
|
-
console.log(`[${pluginName}] - Copy ${src} to ${dest} - dest is null`)
|
124
|
-
return;
|
125
|
-
}
|
126
|
-
var exists = existsSync(src);
|
127
|
-
var stats = exists && statSync(src);
|
128
|
-
var isDirectory = exists && stats.isDirectory();
|
129
|
-
if (isDirectory) {
|
130
|
-
if (!existsSync(dest))
|
131
|
-
mkdirSync(dest, { recursive: true });
|
132
|
-
readdirSync(src).forEach(function (childItemName) {
|
133
|
-
copyRecursiveSync(join(src, childItemName), join(dest, childItemName));
|
134
|
-
});
|
135
|
-
} else {
|
136
|
-
copyFileSync(src, dest);
|
137
|
-
}
|
138
|
-
};
|
139
|
+
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { loadConfig } from "./config.js";
|
2
2
|
import { tryGetNeedleEngineVersion } from "../common/version.js";
|
3
|
+
import { getPublicIdentifier as getPublicIdentifier } from "../common/license.js";
|
3
4
|
|
4
5
|
// NOTE: ALL DEFINES MUST BE SET HERE! NEVER ADD OR RELY ON DEFINES IN ANY OTHER PLUGIN
|
5
6
|
|
@@ -22,7 +23,7 @@
|
|
22
23
|
return {
|
23
24
|
name: 'needle:defines',
|
24
25
|
enforce: 'pre',
|
25
|
-
config(viteConfig) {
|
26
|
+
async config(viteConfig) {
|
26
27
|
// console.log("Update vite defines -------------------------------------------");
|
27
28
|
if (!viteConfig.define) viteConfig.define = {};
|
28
29
|
const version = tryGetNeedleEngineVersion();
|
@@ -45,6 +46,12 @@
|
|
45
46
|
|
46
47
|
// this gives a timestamp containing the timezone
|
47
48
|
viteConfig.define.NEEDLE_PROJECT_BUILD_TIME = "\"" + new Date().toString() + "\"";
|
49
|
+
|
50
|
+
const projectId = undefined; // TODO: this needs to be exported by the integration (if any)
|
51
|
+
const publicIdentifier = await getPublicIdentifier(projectId).catch(console.warn);
|
52
|
+
if(publicIdentifier){
|
53
|
+
viteConfig.define.NEEDLE_PUBLIC_KEY = "\"" + publicIdentifier + "\"";
|
54
|
+
}
|
48
55
|
}
|
49
56
|
}
|
50
57
|
}
|
@@ -7,28 +7,90 @@
|
|
7
7
|
* @param {import('../types').userSettings} userSettings
|
8
8
|
*/
|
9
9
|
export const needleDependencies = (command, config, userSettings) => {
|
10
|
+
|
11
|
+
const handleChunks = true;
|
12
|
+
|
10
13
|
/**
|
11
14
|
* @type {import('vite').Plugin}
|
12
15
|
*/
|
13
16
|
return {
|
14
17
|
name: 'needle:dependencies',
|
15
18
|
enforce: 'pre',
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
config.optimizeDeps = {};
|
23
|
-
}
|
24
|
-
if (!config.optimizeDeps.exclude) {
|
25
|
-
config.optimizeDeps.exclude = [];
|
26
|
-
}
|
27
|
-
console.log("[needle-dependencies] Adding three-mesh-bvh to the optimizeDeps.exclude array.");
|
28
|
-
// This needs to be excluded from optimization because otherwise the worker import fails
|
29
|
-
// three-mesh-bvh/src/workers/generateMeshBVH.worker.js?worker
|
30
|
-
config.optimizeDeps.exclude.push("three-mesh-bvh");
|
31
|
-
}
|
19
|
+
/**
|
20
|
+
* @param {import('vite').UserConfig} config
|
21
|
+
*/
|
22
|
+
config: (config, env) => {
|
23
|
+
handleOptimizeDeps(config);
|
24
|
+
handleManualChunks(config);
|
32
25
|
}
|
33
26
|
}
|
34
27
|
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* @param {import('vite').UserConfig} config
|
31
|
+
*/
|
32
|
+
function handleOptimizeDeps(config) {
|
33
|
+
if (config.optimizeDeps?.include?.includes("three-mesh-bvh")) {
|
34
|
+
console.log("[needle-dependencies] three-mesh-bvh is included in the optimizeDeps.include array. This may cause issues with the worker import.");
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
if (!config.optimizeDeps) {
|
38
|
+
config.optimizeDeps = {};
|
39
|
+
}
|
40
|
+
if (!config.optimizeDeps.exclude) {
|
41
|
+
config.optimizeDeps.exclude = [];
|
42
|
+
}
|
43
|
+
console.log("[needle-dependencies] Adding three-mesh-bvh to the optimizeDeps.exclude array.");
|
44
|
+
// This needs to be excluded from optimization because otherwise the worker import fails
|
45
|
+
// three-mesh-bvh/src/workers/generateMeshBVH.worker.js?worker
|
46
|
+
config.optimizeDeps.exclude.push("three-mesh-bvh");
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* @param {import('vite').UserConfig} config
|
52
|
+
*/
|
53
|
+
function handleManualChunks(config) {
|
54
|
+
if (!config.build) {
|
55
|
+
config.build = {};
|
56
|
+
}
|
57
|
+
if (!config.build.rollupOptions) {
|
58
|
+
config.build.rollupOptions = {};
|
59
|
+
}
|
60
|
+
if (!config.build.rollupOptions.output) {
|
61
|
+
config.build.rollupOptions.output = {};
|
62
|
+
}
|
63
|
+
|
64
|
+
const rollupOutput = config.build.rollupOptions.output;
|
65
|
+
|
66
|
+
if (Array.isArray(rollupOutput)) {
|
67
|
+
// append the manualChunks function to the array
|
68
|
+
console.log("[needle-dependencies] registering manualChunks");
|
69
|
+
rollupOutput.push({
|
70
|
+
manualChunks: needleManualChunks
|
71
|
+
})
|
72
|
+
}
|
73
|
+
else {
|
74
|
+
if (rollupOutput.manualChunks) {
|
75
|
+
// if the user has already defined manualChunks, we don't want to overwrite it
|
76
|
+
console.log("[needle-dependencies] manualChunks already defined");
|
77
|
+
}
|
78
|
+
else {
|
79
|
+
console.log("[needle-dependencies] registering manualChunks");
|
80
|
+
rollupOutput.manualChunks = needleManualChunks;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
function needleManualChunks(id) {
|
85
|
+
// Push the pmndrs postprocessing package into a separate postprocessing chunk
|
86
|
+
if (id.includes("node_modules/postprocessing/" || id.includes("node_modules/n8ao"))) {
|
87
|
+
return "postprocessing";
|
88
|
+
}
|
89
|
+
else if (id.includes("node_modules/three-mesh-ui")) {
|
90
|
+
return "three-mesh-ui";
|
91
|
+
}
|
92
|
+
else if (id.includes("@dimforge/rapier3d")) {
|
93
|
+
return "rapier3d";
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
@@ -7,8 +7,13 @@
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
8
8
|
|
9
9
|
/** experimental, allow dropping files from Unity into the running scene */
|
10
|
+
/**
|
11
|
+
* @param {import('../types/userconfig.js').userSettings} userSettings
|
12
|
+
*/
|
10
13
|
export const needleDrop = (command, config, userSettings) => {
|
11
14
|
if (command === "build") return;
|
15
|
+
|
16
|
+
if(userSettings.useDrop !== true) return null;
|
12
17
|
|
13
18
|
return {
|
14
19
|
name: "needle:drop",
|
@@ -96,7 +96,7 @@
|
|
96
96
|
}
|
97
97
|
* ```
|
98
98
|
* @param {string} command
|
99
|
-
* @param {import('../types').userSettings} userSettings
|
99
|
+
* @param {import('../types/index.js').userSettings} userSettings
|
100
100
|
*/
|
101
101
|
export const needlePlugins = async (command, config, userSettings) => {
|
102
102
|
|
@@ -1,11 +1,11 @@
|
|
1
|
-
import { resolveLicense } from '../common/license.
|
1
|
+
import { resolveLicense } from '../common/license.js';
|
2
2
|
import { loadConfig } from './config.js';
|
3
3
|
|
4
4
|
/**
|
5
5
|
* This plugin is used to apply the license to the needle engine.
|
6
6
|
* @param {string} command - The command that is being run
|
7
7
|
* @param {object} config - The config object
|
8
|
-
* @param {import('../types/userconfig.js').userSettings}
|
8
|
+
* @param {import('../types/userconfig.js').userSettings} userSettings
|
9
9
|
*/
|
10
10
|
export const needleLicense = (command, config, userSettings) => {
|
11
11
|
let license = undefined;
|
@@ -14,17 +14,20 @@
|
|
14
14
|
name: "needle:license",
|
15
15
|
enforce: 'pre',
|
16
16
|
async configResolved() {
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
const needleConfig = await loadConfig();
|
24
|
-
if (needleConfig) {
|
25
|
-
license = await resolveLicense(needleConfig.license);
|
17
|
+
|
18
|
+
let team = userSettings?.license?.team;
|
19
|
+
if (!team) {
|
20
|
+
const meta = await loadConfig(null);
|
21
|
+
if (meta) {
|
22
|
+
team = meta.license?.team;
|
26
23
|
}
|
27
24
|
}
|
25
|
+
|
26
|
+
license = await resolveLicense({
|
27
|
+
team: team,
|
28
|
+
accessToken: userSettings?.license?.accessToken
|
29
|
+
});
|
30
|
+
|
28
31
|
},
|
29
32
|
async transform(src, id) {
|
30
33
|
const isNeedleEngineFile = id.includes("engine/engine_license") || id.includes("needle-tools_engine");
|
@@ -42,6 +42,8 @@
|
|
42
42
|
/** @param {import ('next').NextConfig config } */
|
43
43
|
function nextWebPack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
|
44
44
|
|
45
|
+
// TODO: get public identifier key from license server
|
46
|
+
|
45
47
|
const meta = getMeta();
|
46
48
|
let useRapier = true;
|
47
49
|
if (userSettings.useRapier === false) useRapier = false;
|
@@ -71,6 +71,7 @@
|
|
71
71
|
if (config?.allowHotReload === false) return html;
|
72
72
|
if (userSettings?.allowHotReload === false) return html;
|
73
73
|
const file = path.join(__dirname, 'reload-client.js');
|
74
|
+
const content = readFileSync(file, 'utf8');
|
74
75
|
return {
|
75
76
|
html,
|
76
77
|
tags: [
|
@@ -79,7 +80,7 @@
|
|
79
80
|
attrs: {
|
80
81
|
type: 'module',
|
81
82
|
},
|
82
|
-
children:
|
83
|
+
children: content,
|
83
84
|
injectTo: 'body',
|
84
85
|
},
|
85
86
|
]
|
@@ -52,8 +52,9 @@
|
|
52
52
|
import "./AnimationUtilsAutoplay.js"
|
53
53
|
|
54
54
|
export { DragMode } from "./DragControls.js"
|
55
|
+
export type { DropListenerNetworkEventArguments } from "./DropListener.js";
|
55
56
|
export * from "./particlesystem/api.js"
|
56
57
|
|
57
58
|
// for correct type resolution in JSDoc
|
58
59
|
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
|
59
|
-
import type { Animation } from "./Animation.js";
|
60
|
+
import type { Animation } from "./Animation.js";
|
@@ -47,6 +47,7 @@
|
|
47
47
|
export * from "./engine_math.js";
|
48
48
|
export * from "./engine_networking.js";
|
49
49
|
export { syncField } from "./engine_networking_auto.js";
|
50
|
+
export * from "./engine_networking_blob.js";
|
50
51
|
export * from "./engine_networking_files.js";
|
51
52
|
export * from "./engine_networking_instantiate.js";
|
52
53
|
export * from "./engine_networking_peer.js";
|
@@ -73,7 +74,6 @@
|
|
73
74
|
export * from "./engine_utils.js";
|
74
75
|
export * from "./engine_utils_format.js";
|
75
76
|
export * from "./engine_utils_screenshot.js";
|
76
|
-
export * from "./engine_web_api.js";
|
77
77
|
export * from "./export/index.js";
|
78
78
|
export * from "./extensions/index.js";
|
79
79
|
export * from "./js-extensions/index.js";
|
@@ -6,7 +6,6 @@
|
|
6
6
|
import * as loaders from "../engine/engine_loaders.js"
|
7
7
|
import { Context } from "../engine/engine_setup.js";
|
8
8
|
import * as utils from "../engine/engine_utils.js"
|
9
|
-
import { download_file } from "../engine/engine_web_api.js";
|
10
9
|
import { GameObject } from "./Component.js";
|
11
10
|
|
12
11
|
const debug = utils.getParam("debugavatar");
|
@@ -100,8 +99,8 @@
|
|
100
99
|
if (blob) bin = await blob.arrayBuffer();
|
101
100
|
}
|
102
101
|
if (!bin) {
|
103
|
-
bin = await
|
104
|
-
|
102
|
+
// bin = await BlobStorage.download(avatarId, avatarId, 0, "no url here go away", true);
|
103
|
+
return null;
|
105
104
|
}
|
106
105
|
|
107
106
|
const gltf = await getLoader().parseSync(context, bin, null!, 0);
|
@@ -74,7 +74,7 @@
|
|
74
74
|
}
|
75
75
|
|
76
76
|
@serializable(EventList)
|
77
|
-
onClick: EventList = new EventList();
|
77
|
+
onClick: EventList<void> = new EventList();
|
78
78
|
|
79
79
|
private _isHovered: number = 0;
|
80
80
|
|
@@ -61,6 +61,7 @@
|
|
61
61
|
export { DocumentExtension } from "../export/usdz/extensions/DocumentExtension.js";
|
62
62
|
export { DragControls } from "../DragControls.js";
|
63
63
|
export { DropListener } from "../DropListener.js";
|
64
|
+
export { DropListenerAddedEvent } from "../DropListener.js";
|
64
65
|
export { Duplicatable } from "../Duplicatable.js";
|
65
66
|
export { EffectWrapper } from "../postprocessing/Effects/EffectWrapper.js";
|
66
67
|
export { EmissionModule } from "../particlesystem/ParticleSystemModules.js";
|
@@ -1,16 +1,20 @@
|
|
1
|
-
import { AxesHelper, Box3, Object3D, Vector2, Vector3 } from "three";
|
1
|
+
import { AxesHelper, Box3, Cache, Object3D, Vector2, Vector3 } from "three";
|
2
2
|
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
3
|
|
4
4
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
5
|
import { AnimationUtils } from "../engine/engine_animation.js";
|
6
6
|
import { addComponent } from "../engine/engine_components.js";
|
7
|
+
import { Context } from "../engine/engine_context.js";
|
7
8
|
import { destroy } from "../engine/engine_gameobject.js";
|
8
9
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
9
|
-
import
|
10
|
+
import { getLoader } from "../engine/engine_gltf.js";
|
11
|
+
import { BlobStorage } from "../engine/engine_networking_blob.js";
|
12
|
+
import { PreviewHelper } from "../engine/engine_networking_files.js";
|
13
|
+
import { generateSeed, InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
10
14
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
11
|
-
import { fitObjectIntoVolume, placeOnSurface } from "../engine/engine_three_utils.js";
|
12
|
-
import { Vec3 } from "../engine/engine_types.js";
|
13
|
-
import { getParam } from "../engine/engine_utils.js";
|
15
|
+
import { fitObjectIntoVolume, getBoundingBox, placeOnSurface } from "../engine/engine_three_utils.js";
|
16
|
+
import { IGameObject, Model, Vec3 } from "../engine/engine_types.js";
|
17
|
+
import { getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
14
18
|
import { Animation } from "./Animation.js";
|
15
19
|
import { Behaviour } from "./Component.js";
|
16
20
|
import { EventList } from "./EventList.js";
|
@@ -31,14 +35,43 @@
|
|
31
35
|
declare type DropContext = {
|
32
36
|
screenposition: Vector2;
|
33
37
|
url?: string,
|
38
|
+
file?: File;
|
39
|
+
point?: Vec3;
|
40
|
+
size?: Vec3;
|
34
41
|
}
|
35
42
|
|
36
|
-
|
43
|
+
|
44
|
+
/** Networking event arguments for the DropListener component */
|
45
|
+
export declare type DropListenerNetworkEventArguments = {
|
37
46
|
guid: string,
|
47
|
+
name: string,
|
38
48
|
url: string | string[],
|
49
|
+
/** Worldspace point where the object was placed in the scene */
|
39
50
|
point: Vec3;
|
51
|
+
/** Bounding box size */
|
52
|
+
size: Vec3;
|
53
|
+
contentMD5: string;
|
40
54
|
}
|
41
55
|
|
56
|
+
declare type AddedEventArguments = {
|
57
|
+
sender: DropListener,
|
58
|
+
/** the root object added to the scene */
|
59
|
+
object: Object3D,
|
60
|
+
/** The whole dropped model */
|
61
|
+
model: Model,
|
62
|
+
contentMD5: string;
|
63
|
+
dropped: URL | File | undefined;
|
64
|
+
}
|
65
|
+
|
66
|
+
/** Dispatched when an object is dropped/changed */
|
67
|
+
export class DropListenerAddedEvent<T extends AddedEventArguments> extends CustomEvent<T> {
|
68
|
+
constructor(detail: T) {
|
69
|
+
super(DropListenerEvents.ObjectAdded, { detail });
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
const blobKeyName = "blob";
|
74
|
+
|
42
75
|
/** The DropListener component is used to listen for drag and drop events in the browser and add the dropped files to the scene
|
43
76
|
* It can be used to allow users to drag and drop glTF files into the scene to add new objects.
|
44
77
|
*
|
@@ -70,6 +103,12 @@
|
|
70
103
|
export class DropListener extends Behaviour {
|
71
104
|
|
72
105
|
/**
|
106
|
+
* When enabled the DropListener will automatically network dropped files to other clients.
|
107
|
+
*/
|
108
|
+
@serializable()
|
109
|
+
useNetworking: boolean = true;
|
110
|
+
|
111
|
+
/**
|
73
112
|
* When assigned the Droplistener will only accept files that are dropped on this object.
|
74
113
|
*/
|
75
114
|
@serializable(Object3D)
|
@@ -95,61 +134,87 @@
|
|
95
134
|
placeAtHitPosition: boolean = true;
|
96
135
|
|
97
136
|
|
137
|
+
/**
|
138
|
+
* Invoked after a file has been **added** to the scene.
|
139
|
+
* Arguments are {@link AddedEventArguments}
|
140
|
+
* @event object-added
|
141
|
+
* @param {AddedEventArguments} evt
|
142
|
+
* @example
|
143
|
+
* ```typescript
|
144
|
+
* dropListener.onDropped.addEventListener((evt) => {
|
145
|
+
* console.log("Object added", evt.model);
|
146
|
+
* });
|
147
|
+
*/
|
98
148
|
@serializable(EventList)
|
99
|
-
onDropped: EventList = new EventList();
|
149
|
+
onDropped: EventList<AddedEventArguments> = new EventList();
|
100
150
|
|
101
|
-
|
102
151
|
/** @internal */
|
103
152
|
onEnable(): void {
|
104
153
|
this.context.renderer.domElement.addEventListener("dragover", this.onDrag);
|
105
154
|
this.context.renderer.domElement.addEventListener("drop", this.onDrop);
|
106
|
-
window.addEventListener("
|
155
|
+
window.addEventListener("paste", this.handlePaste);
|
107
156
|
this.context.connection.beginListen("droplistener", this.onNetworkEvent)
|
108
157
|
}
|
109
158
|
/** @internal */
|
110
159
|
onDisable(): void {
|
111
160
|
this.context.renderer.domElement.removeEventListener("dragover", this.onDrag);
|
112
161
|
this.context.renderer.domElement.removeEventListener("drop", this.onDrop);
|
113
|
-
window.removeEventListener("
|
114
|
-
this.context.connection.stopListen("droplistener", this.onNetworkEvent)
|
162
|
+
window.removeEventListener("paste", this.handlePaste);
|
163
|
+
this.context.connection.stopListen("droplistener", this.onNetworkEvent);
|
115
164
|
}
|
116
165
|
|
117
|
-
|
166
|
+
/**
|
167
|
+
* Loads a file from the given URL and adds it to the scene.
|
168
|
+
*/
|
169
|
+
loadFromURL(url: string, data?: { point?: Vec3, size?: Vec3 }) {
|
170
|
+
this.addFromUrl(url, { screenposition: new Vector2(), point: data?.point, size: data?.size, }, true);
|
171
|
+
}
|
172
|
+
|
173
|
+
/**
|
174
|
+
* Forgets all previously added objects.
|
175
|
+
* The droplistener will then not be able to remove previously added objects.
|
176
|
+
*/
|
177
|
+
forgetObjects() {
|
178
|
+
this.removePreviouslyAddedObjects(false);
|
179
|
+
}
|
180
|
+
|
181
|
+
private onNetworkEvent = (evt: DropListenerNetworkEventArguments) => {
|
182
|
+
if (!this.useNetworking) {
|
183
|
+
if (debug) console.debug("[DropListener] Ignoring networked event because networking is disabled", evt);
|
184
|
+
return;
|
185
|
+
}
|
118
186
|
if (evt.guid?.startsWith(this.guid)) {
|
119
187
|
const url = evt.url;
|
188
|
+
console.debug("[DropListener] Received networked event", evt);
|
120
189
|
if (url) {
|
121
190
|
if (Array.isArray(url)) {
|
122
191
|
for (const _url of url) {
|
123
|
-
this.addFromUrl(_url, { screenposition: new Vector2() }, true)
|
124
|
-
res?.position.set(evt.point.x, evt.point.y, evt.point.z);
|
125
|
-
})
|
192
|
+
this.addFromUrl(_url, { screenposition: new Vector2(), point: evt.point, size: evt.size, }, true);
|
126
193
|
}
|
127
194
|
}
|
128
195
|
else {
|
129
|
-
this.addFromUrl(url, { screenposition: new Vector2() }, true)
|
130
|
-
res?.position.set(evt.point.x, evt.point.y, evt.point.z);
|
131
|
-
})
|
196
|
+
this.addFromUrl(url, { screenposition: new Vector2(), point: evt.point, size: evt.size }, true);
|
132
197
|
}
|
133
198
|
}
|
134
199
|
}
|
135
200
|
}
|
136
201
|
|
137
|
-
private handlePaste =
|
202
|
+
private handlePaste = (evt: Event) => {
|
138
203
|
if (this.context.connection.allowEditing === false) return;
|
139
|
-
|
140
|
-
const
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
204
|
+
if (evt.defaultPrevented) return;
|
205
|
+
const clipboard = navigator.clipboard;
|
206
|
+
clipboard.readText()
|
207
|
+
.then(value => {
|
208
|
+
if (value) {
|
209
|
+
const isUrl = value.startsWith("http") || value.startsWith("https") || value.startsWith("blob");
|
210
|
+
if (isUrl) {
|
211
|
+
const ctx = { screenposition: new Vector2(this.context.input.mousePosition.x, this.context.input.mousePosition.y) };
|
212
|
+
if (this.testIfIsInDropArea(ctx))
|
213
|
+
this.addFromUrl(value, ctx, false);
|
214
|
+
}
|
150
215
|
}
|
151
|
-
}
|
152
|
-
|
216
|
+
})
|
217
|
+
.catch(console.warn);
|
153
218
|
}
|
154
219
|
|
155
220
|
private onDrag = (evt: DragEvent) => {
|
@@ -182,12 +247,13 @@
|
|
182
247
|
const items = evt.dataTransfer.items;
|
183
248
|
if (!items) return;
|
184
249
|
|
250
|
+
const files: File[] = [];
|
185
251
|
for (const ite in items) {
|
186
252
|
const it = items[ite];
|
187
253
|
if (it.kind === "file") {
|
188
254
|
const file = it.getAsFile();
|
189
255
|
if (!file) continue;
|
190
|
-
|
256
|
+
files.push(file);
|
191
257
|
}
|
192
258
|
else if (it.kind === "string" && it.type == "text/plain") {
|
193
259
|
it.getAsString(str => {
|
@@ -195,6 +261,9 @@
|
|
195
261
|
});
|
196
262
|
}
|
197
263
|
}
|
264
|
+
if (files.length > 0) {
|
265
|
+
await this.addDroppedFiles(files, ctx);
|
266
|
+
}
|
198
267
|
}
|
199
268
|
|
200
269
|
private async addFromUrl(url: string, ctx: DropContext, isRemote: boolean) {
|
@@ -221,8 +290,17 @@
|
|
221
290
|
return null;
|
222
291
|
}
|
223
292
|
|
224
|
-
|
225
|
-
|
293
|
+
// TODO: if the URL is invalid this will become a problem
|
294
|
+
this.removePreviouslyAddedObjects();
|
295
|
+
// const binary = await fetch(url).then(res => res.arrayBuffer());
|
296
|
+
const res = await FileHelper.loadFileFromURL(new URL(url), {
|
297
|
+
guid: this.guid,
|
298
|
+
context: this.context,
|
299
|
+
parent: this.gameObject,
|
300
|
+
point: ctx.point,
|
301
|
+
size: ctx.size,
|
302
|
+
});
|
303
|
+
if (res && this._addedObjects.length <= 0) {
|
226
304
|
ctx.url = url;
|
227
305
|
const obj = this.addObject(res, ctx, isRemote);
|
228
306
|
return obj;
|
@@ -235,50 +313,92 @@
|
|
235
313
|
return null;
|
236
314
|
}
|
237
315
|
|
238
|
-
private
|
316
|
+
private _abort: AbortController | null = null;
|
317
|
+
|
318
|
+
private async addDroppedFiles(fileList: Array<File>, ctx: DropContext) {
|
239
319
|
if (debug) console.log("Add files", fileList)
|
240
320
|
if (!Array.isArray(fileList)) return;
|
241
321
|
if (!fileList.length) return;
|
242
322
|
|
323
|
+
this.deleteDropEvent();
|
324
|
+
this.removePreviouslyAddedObjects();
|
325
|
+
setParamWithoutReload(blobKeyName, null);
|
326
|
+
|
327
|
+
// Create an abort controller for the current drop operation
|
328
|
+
this._abort?.abort("New files dropped");
|
329
|
+
this._abort = new AbortController();
|
330
|
+
|
243
331
|
for (const file of fileList) {
|
244
332
|
if (!file) continue;
|
245
|
-
|
246
|
-
const res = await
|
247
|
-
|
248
|
-
|
249
|
-
|
333
|
+
console.debug("Load file " + file.name);
|
334
|
+
const res = await FileHelper.loadFile(file, this.context, { guid: this.guid });
|
335
|
+
if (res) {
|
336
|
+
this.dispatchEvent(new CustomEvent(DropListenerEvents.FileDropped, { detail: file }));
|
337
|
+
ctx.file = file;
|
338
|
+
const obj = this.addObject(res, ctx, false);
|
339
|
+
|
340
|
+
// handle uploading the dropped object and networking the event
|
341
|
+
if (obj && this.context.connection.isConnected && this.useNetworking) {
|
342
|
+
console.debug("Uploading dropped file to blob storage");
|
343
|
+
BlobStorage.upload(file, { abort: this._abort?.signal, })
|
344
|
+
.then(upload => {
|
345
|
+
// check if the upload was successful and if the object should still be visible
|
346
|
+
if (upload?.download_url && this._addedObjects.includes(obj)) {
|
347
|
+
// setParamWithoutReload(blobKeyName, upload.key);
|
348
|
+
this.sendDropEvent(upload.download_url, obj, res.contentMD5);
|
349
|
+
}
|
350
|
+
})
|
351
|
+
.catch(console.warn);
|
352
|
+
}
|
353
|
+
|
354
|
+
// we currently only support dropping one file
|
355
|
+
break;
|
356
|
+
}
|
250
357
|
}
|
251
358
|
}
|
252
359
|
|
253
360
|
/** Previously added objects */
|
254
361
|
private readonly _addedObjects = new Array<Object3D>();
|
362
|
+
private readonly _addedModels = new Array<Model>();
|
255
363
|
|
256
|
-
|
257
|
-
|
258
|
-
if (
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
for (const prev of this._addedObjects) {
|
264
|
-
if (prev.parent === this.gameObject) {
|
265
|
-
destroy(prev, true, true);
|
364
|
+
/** Removes all previously added objects from the scene and removes those object references */
|
365
|
+
private removePreviouslyAddedObjects(doDestroy: boolean = true) {
|
366
|
+
if (doDestroy) {
|
367
|
+
for (const prev of this._addedObjects) {
|
368
|
+
if (prev.parent === this.gameObject) {
|
369
|
+
destroy(prev, true, true);
|
370
|
+
}
|
266
371
|
}
|
267
372
|
}
|
268
373
|
this._addedObjects.length = 0;
|
374
|
+
this._addedModels.length = 0;
|
375
|
+
}
|
269
376
|
|
270
|
-
|
377
|
+
/**
|
378
|
+
* Adds the object to the scene and fits it into the volume if {@link fitIntoVolume} is enabled.
|
379
|
+
*/
|
380
|
+
private addObject(data: { model: Model, contentMD5: string }, ctx: DropContext, isRemote: boolean): Object3D | null {
|
271
381
|
|
382
|
+
const { model, contentMD5 } = data;
|
383
|
+
|
384
|
+
if (debug) console.log(`Dropped ${this.gameObject.name}`, model);
|
385
|
+
if (!model?.scene) {
|
386
|
+
console.warn("No object specified to add to scene", model);
|
387
|
+
return null;
|
388
|
+
}
|
389
|
+
|
390
|
+
this.removePreviouslyAddedObjects();
|
391
|
+
|
392
|
+
const obj = model.scene;
|
393
|
+
|
272
394
|
// use attach to ignore the DropListener scale (e.g. if the parent object scale is not uniform)
|
273
395
|
this.gameObject.attach(obj);
|
274
396
|
obj.position.set(0, 0, 0);
|
275
397
|
obj.quaternion.identity();
|
276
398
|
|
277
399
|
this._addedObjects.push(obj);
|
400
|
+
this._addedModels.push(model);
|
278
401
|
|
279
|
-
if (debug) obj.add(new AxesHelper(1));
|
280
|
-
|
281
|
-
|
282
402
|
const volume = new Box3().setFromCenterAndSize(new Vector3(0, this.fitVolumeSize.y * .5, 0).add(this.gameObject.worldPosition), this.fitVolumeSize);
|
283
403
|
if (debug) Gizmos.DrawWireBox3(volume, 0x0000ff, 5);
|
284
404
|
if (this.fitIntoVolume) {
|
@@ -301,25 +421,51 @@
|
|
301
421
|
}
|
302
422
|
}
|
303
423
|
|
304
|
-
AnimationUtils.assignAnimationsFromFile(
|
424
|
+
AnimationUtils.assignAnimationsFromFile(model, {
|
305
425
|
createAnimationComponent: obj => addComponent(obj, Animation)
|
306
426
|
});
|
307
427
|
|
308
|
-
|
309
|
-
|
428
|
+
const evt = new DropListenerAddedEvent({
|
429
|
+
sender: this,
|
430
|
+
gltf: model,
|
431
|
+
model: model,
|
432
|
+
object: obj,
|
433
|
+
contentMD5: contentMD5,
|
434
|
+
dropped: ctx.file || (ctx.url ? new URL(ctx.url) : undefined),
|
435
|
+
});
|
436
|
+
this.dispatchEvent(evt);
|
437
|
+
this.onDropped?.invoke(evt.detail);
|
310
438
|
|
311
439
|
// send network event
|
312
440
|
if (!isRemote && ctx.url?.startsWith("http") && this.context.connection.isConnected && obj) {
|
313
|
-
|
441
|
+
this.sendDropEvent(ctx.url, obj, contentMD5);
|
442
|
+
}
|
443
|
+
|
444
|
+
return obj;
|
445
|
+
}
|
446
|
+
|
447
|
+
private async sendDropEvent(url: string, obj: Object3D, contentmd5: string) {
|
448
|
+
if (!this.useNetworking) {
|
449
|
+
if (debug) console.debug("[DropListener] Ignoring networked event because networking is disabled", url);
|
450
|
+
return;
|
451
|
+
}
|
452
|
+
if (this.context.connection.isConnected) {
|
453
|
+
console.debug("Sending drop event \"" + obj.name + "\"", url);
|
454
|
+
const bounds = getBoundingBox([obj]);
|
455
|
+
const evt: DropListenerNetworkEventArguments = {
|
456
|
+
name: obj.name,
|
314
457
|
guid: this.guid,
|
315
|
-
url
|
316
|
-
point: obj.
|
458
|
+
url,
|
459
|
+
point: obj.worldPosition.clone(),
|
460
|
+
size: bounds.getSize(new Vector3()),
|
461
|
+
contentMD5: contentmd5,
|
317
462
|
};
|
318
463
|
this.context.connection.send("droplistener", evt);
|
319
464
|
}
|
320
|
-
|
321
|
-
return obj;
|
322
465
|
}
|
466
|
+
private deleteDropEvent() {
|
467
|
+
this.context.connection.sendDeleteRemoteState(this.guid);
|
468
|
+
}
|
323
469
|
|
324
470
|
|
325
471
|
|
@@ -331,6 +477,7 @@
|
|
331
477
|
screenPoint,
|
332
478
|
recursive: true,
|
333
479
|
testObject: obj => {
|
480
|
+
// Ignore hits on the already added objects, they don't count as part of the dropzone
|
334
481
|
if (this._addedObjects.includes(obj)) return false;
|
335
482
|
return true;
|
336
483
|
}
|
@@ -357,4 +504,76 @@
|
|
357
504
|
console.log("Resolved polyhaven asset url", urlStr, "→", assetUrl);
|
358
505
|
// TODO: need to resolve textures properly
|
359
506
|
return assetUrl;
|
507
|
+
}
|
508
|
+
|
509
|
+
|
510
|
+
|
511
|
+
namespace FileHelper {
|
512
|
+
|
513
|
+
export async function loadFile(file: File, context: Context, args: { guid: string }): Promise<{ model: Model, contentMD5: string } | null> {
|
514
|
+
const name = file.name.toLowerCase();
|
515
|
+
if (name.endsWith(".gltf") ||
|
516
|
+
name.endsWith(".glb") ||
|
517
|
+
name.endsWith(".fbx") ||
|
518
|
+
name.endsWith(".obj") ||
|
519
|
+
name.endsWith(".usdz") ||
|
520
|
+
name.endsWith(".vrm") ||
|
521
|
+
file.type === "model/gltf+json" ||
|
522
|
+
file.type === "model/gltf-binary"
|
523
|
+
) {
|
524
|
+
return new Promise((resolve, _reject) => {
|
525
|
+
const reader = new FileReader()
|
526
|
+
reader.readAsArrayBuffer(file);
|
527
|
+
reader.onloadend = async (_ev: ProgressEvent<FileReader>) => {
|
528
|
+
const content = reader.result as ArrayBuffer;
|
529
|
+
// first load it locally
|
530
|
+
const seed = args.guid;
|
531
|
+
const prov = new InstantiateIdProvider(seed);
|
532
|
+
const model = await getLoader().parseSync(context, content, file.name, prov);
|
533
|
+
if (model) {
|
534
|
+
const hash = BlobStorage.hashMD5(content);
|
535
|
+
resolve({ model, contentMD5: hash });
|
536
|
+
}
|
537
|
+
};
|
538
|
+
});
|
539
|
+
}
|
540
|
+
else {
|
541
|
+
console.warn("Unsupported file type: " + name, file.type)
|
542
|
+
}
|
543
|
+
|
544
|
+
return null;
|
545
|
+
}
|
546
|
+
|
547
|
+
export async function loadFileFromURL(url: URL, args: { guid: string, context: Context, parent: Object3D, point?: Vec3, size?: Vec3 }): Promise<{ model: Model, contentMD5: string } | null> {
|
548
|
+
return new Promise(async (resolve, _reject) => {
|
549
|
+
|
550
|
+
const prov = new InstantiateIdProvider(args.guid);
|
551
|
+
const urlStr = url.toString();
|
552
|
+
|
553
|
+
if (debug) Gizmos.DrawWireSphere(args.point!, .1, 0xff0000, 3);
|
554
|
+
const preview = PreviewHelper.addPreview({
|
555
|
+
guid: args.guid,
|
556
|
+
parent: args.parent,
|
557
|
+
position: args?.point,
|
558
|
+
size: args?.size,
|
559
|
+
});
|
560
|
+
|
561
|
+
const model = await getLoader().loadSync(args.context, urlStr, urlStr, prov, prog => {
|
562
|
+
preview.onProgress(prog.loaded / prog.total);
|
563
|
+
}).catch(console.warn);
|
564
|
+
|
565
|
+
if (model) {
|
566
|
+
const binary = await fetch(urlStr).then(res => res.arrayBuffer());
|
567
|
+
const hash = BlobStorage.hashMD5(binary);
|
568
|
+
if (debug) setTimeout(() => PreviewHelper.removePreview(args.guid), 3000);
|
569
|
+
else PreviewHelper.removePreview(args.guid);
|
570
|
+
resolve({ model, contentMD5: hash });
|
571
|
+
}
|
572
|
+
else {
|
573
|
+
if (debug) setTimeout(() => PreviewHelper.removePreview(args.guid), 3000);
|
574
|
+
else PreviewHelper.removePreview(args.guid);
|
575
|
+
console.warn("Unsupported file type: " + url.toString());
|
576
|
+
}
|
577
|
+
});
|
578
|
+
}
|
360
579
|
}
|
@@ -4,11 +4,11 @@
|
|
4
4
|
import { destroy, type IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
|
5
5
|
import { getLoader } from "./engine_gltf.js";
|
6
6
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
7
|
+
import { BlobStorage } from "./engine_networking_blob.js";
|
7
8
|
import { registerPrefabProvider, syncInstantiate,SyncInstantiateOptions } from "./engine_networking_instantiate.js";
|
8
9
|
import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
|
9
10
|
import { Context } from "./engine_setup.js";
|
10
11
|
import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
|
11
|
-
import { download } from "./engine_web_api.js";
|
12
12
|
|
13
13
|
const debug = getParam("debugaddressables");
|
14
14
|
|
@@ -240,7 +240,7 @@
|
|
240
240
|
if (this._rawBinary !== undefined) return this._rawBinary;
|
241
241
|
this._isLoadingRawBinary = true;
|
242
242
|
if (debug) console.log("Preload", this._hashedUri);
|
243
|
-
const res = await download(this._hashedUri, p => {
|
243
|
+
const res = await BlobStorage.download(this._hashedUri, p => {
|
244
244
|
this.raiseProgressEvent(p);
|
245
245
|
});
|
246
246
|
this._rawBinary = res?.buffer ?? null;
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { AnimationAction, AnimationClip, AnimationMixer, Object3D, PropertyBinding } from "three";
|
2
2
|
|
3
3
|
import type { Context } from "./engine_context.js";
|
4
|
-
import { GLTF, IAnimationComponent } from "./engine_types.js";
|
4
|
+
import { GLTF, IAnimationComponent, Model } from "./engine_types.js";
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Registry for animation related data. Use {@link registerAnimationMixer} to register an animation mixer instance.
|
@@ -88,7 +88,7 @@
|
|
88
88
|
* This method will look for objects in the scene that have animations and assign them to the correct objects.
|
89
89
|
* @param file The GLTF file to assign the animations from
|
90
90
|
*/
|
91
|
-
static assignAnimationsFromFile(file: Pick<
|
91
|
+
static assignAnimationsFromFile(file: Pick<Model, "animations" | "scene">, opts?: { createAnimationComponent(obj: Object3D, animation: AnimationClip): IAnimationComponent }) {
|
92
92
|
if (!file || !file.animations) {
|
93
93
|
console.debug("No animations found in file");
|
94
94
|
return;
|
@@ -1,18 +1,16 @@
|
|
1
1
|
import { type IComponent } from "./engine_types.js";
|
2
2
|
import { getParam } from "./engine_utils.js";
|
3
3
|
|
4
|
-
export enum ComponentEvents {
|
5
|
-
Added = "component-added",
|
6
|
-
Removing = "removing-component"
|
7
|
-
}
|
8
4
|
|
5
|
+
type ComponentLifecycleEvent = "component-added" | "removing-component";
|
6
|
+
|
9
7
|
const debug = getParam("debugcomponentevents");
|
10
8
|
|
11
9
|
export class ComponentLifecycleEvents {
|
12
10
|
|
13
11
|
private static eventListeners = new Map<string, ((data: IComponent) => void)[]>();
|
14
12
|
|
15
|
-
static addComponentLifecylceEventListener(evt: string, cb: (data: IComponent) => void) {
|
13
|
+
static addComponentLifecylceEventListener(evt: ComponentLifecycleEvent | (string & {}), cb: (data: IComponent) => void) {
|
16
14
|
if (this.eventListeners.has(evt)) {
|
17
15
|
this.eventListeners.set(evt, []);
|
18
16
|
}
|
@@ -23,7 +21,7 @@
|
|
23
21
|
if(debug) console.log("Added event listener for " + evt, this.eventListeners)
|
24
22
|
}
|
25
23
|
|
26
|
-
static removeComponentLifecylceEventListener(evt: string, cb: (data: IComponent) => void) {
|
24
|
+
static removeComponentLifecylceEventListener(evt: ComponentLifecycleEvent | (string & {}), cb: (data: IComponent) => void) {
|
27
25
|
const listeners = this.eventListeners.get(evt);
|
28
26
|
if (!listeners) return;
|
29
27
|
const index = listeners.indexOf(cb);
|
@@ -32,7 +30,7 @@
|
|
32
30
|
|
33
31
|
}
|
34
32
|
|
35
|
-
static dispatchComponentLifecycleEvent(evt:
|
33
|
+
static dispatchComponentLifecycleEvent(evt: ComponentLifecycleEvent, data: IComponent) {
|
36
34
|
const listeners = this.eventListeners.get(evt);
|
37
35
|
if(debug) console.log("Dispatching event " + evt, listeners)
|
38
36
|
if (!listeners) return;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { Object3D, Scene } from "three";
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { ComponentLifecycleEvents } from "./engine_components_internal.js";
|
4
4
|
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
5
5
|
import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
|
6
6
|
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
@@ -30,7 +30,7 @@
|
|
30
30
|
const index = go.userData.components.indexOf(componentInstance);
|
31
31
|
if (index < 0) return componentInstance;
|
32
32
|
|
33
|
-
ComponentLifecycleEvents.dispatchComponentLifecycleEvent(
|
33
|
+
ComponentLifecycleEvents.dispatchComponentLifecycleEvent("removing-component", componentInstance);
|
34
34
|
|
35
35
|
//@ts-ignore
|
36
36
|
componentInstance.gameObject = null;
|
@@ -67,7 +67,7 @@
|
|
67
67
|
if (componentInstance.activeAndEnabled)
|
68
68
|
componentInstance.__internalAwake();
|
69
69
|
}
|
70
|
-
ComponentLifecycleEvents.dispatchComponentLifecycleEvent(
|
70
|
+
ComponentLifecycleEvents.dispatchComponentLifecycleEvent("component-added", componentInstance);
|
71
71
|
}
|
72
72
|
catch (err) {
|
73
73
|
console.error(err);
|
@@ -6,18 +6,21 @@
|
|
6
6
|
// 2) Vite where global defines are made, vite defines are also automatically set to globalThis
|
7
7
|
// 3) Webpack where global defines are not made BUT declare const variables are replaces with the actual value (via the webpack DefinePlugin)
|
8
8
|
|
9
|
-
tryEval(`if(!globalThis["NEEDLE_ENGINE_VERSION"]) globalThis["NEEDLE_ENGINE_VERSION"] = "0.0.0";`)
|
10
|
-
tryEval(`if(!globalThis["NEEDLE_ENGINE_GENERATOR"]) globalThis["NEEDLE_ENGINE_GENERATOR"] = "unknown";`)
|
11
|
-
tryEval(`if(!globalThis["NEEDLE_PROJECT_BUILD_TIME"]) globalThis["NEEDLE_PROJECT_BUILD_TIME"] = "unknown";`)
|
9
|
+
tryEval(`if(!globalThis["NEEDLE_ENGINE_VERSION"]) globalThis["NEEDLE_ENGINE_VERSION"] = "0.0.0";`);
|
10
|
+
tryEval(`if(!globalThis["NEEDLE_ENGINE_GENERATOR"]) globalThis["NEEDLE_ENGINE_GENERATOR"] = "unknown";`);
|
11
|
+
tryEval(`if(!globalThis["NEEDLE_PROJECT_BUILD_TIME"]) globalThis["NEEDLE_PROJECT_BUILD_TIME"] = "unknown";`);
|
12
|
+
tryEval(`if(!globalThis["NEEDLE_PUBLIC_KEY"]) globalThis["NEEDLE_PUBLIC_KEY"] = "unknown";`);
|
12
13
|
|
13
14
|
declare const NEEDLE_ENGINE_VERSION: string
|
14
15
|
declare const NEEDLE_ENGINE_GENERATOR: string;
|
15
16
|
declare const NEEDLE_PROJECT_BUILD_TIME: string;
|
17
|
+
declare const NEEDLE_PUBLIC_KEY: string;
|
16
18
|
|
17
19
|
// Make sure to wrap the new global this define in underscores to prevent the bundler from replacing it with the actual value
|
18
|
-
tryEval(`globalThis["__NEEDLE_ENGINE_VERSION__"] = "` + NEEDLE_ENGINE_VERSION + `";`)
|
19
|
-
tryEval(`globalThis["__NEEDLE_ENGINE_GENERATOR__"] = "` + NEEDLE_ENGINE_GENERATOR + `";`)
|
20
|
-
tryEval(`globalThis["__NEEDLE_PROJECT_BUILD_TIME__"] = "` + NEEDLE_PROJECT_BUILD_TIME + `";`)
|
20
|
+
tryEval(`globalThis["__NEEDLE_ENGINE_VERSION__"] = "` + NEEDLE_ENGINE_VERSION + `";`);
|
21
|
+
tryEval(`globalThis["__NEEDLE_ENGINE_GENERATOR__"] = "` + NEEDLE_ENGINE_GENERATOR + `";`);
|
22
|
+
tryEval(`globalThis["__NEEDLE_PROJECT_BUILD_TIME__"] = "` + NEEDLE_PROJECT_BUILD_TIME + `";`);
|
23
|
+
tryEval(`globalThis["__NEEDLE_PUBLIC_KEY__"] = "` + NEEDLE_PUBLIC_KEY + `";`);
|
21
24
|
|
22
25
|
/** The version of the Needle engine */
|
23
26
|
export const VERSION = NEEDLE_ENGINE_VERSION;
|
@@ -27,6 +30,8 @@
|
|
27
30
|
export const BUILD_TIME = NEEDLE_PROJECT_BUILD_TIME;
|
28
31
|
if (debug) console.log(`Engine version: ${VERSION} (generator: ${GENERATOR})\nProject built at ${BUILD_TIME}`);
|
29
32
|
|
33
|
+
export const PUBLIC_KEY = NEEDLE_PUBLIC_KEY;
|
34
|
+
|
30
35
|
export const activeInHierarchyFieldName = "needle_isActiveInHierarchy";
|
31
36
|
export const builtinComponentKeyName = "builtin_components";
|
32
37
|
// It's easier to use a string than a symbol here because the symbol might not be the same when imported in other packages
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { type IComponent, type IContext, type
|
1
|
+
import { type IComponent, type IContext, type LoadedModel } from "./engine_types.js";
|
2
2
|
|
3
3
|
/** The various events that can be dispatched by a Needle Engine {@link IContext} instance
|
4
4
|
*/
|
@@ -26,7 +26,7 @@
|
|
26
26
|
export type ContextEventArgs = {
|
27
27
|
event: ContextEvent;
|
28
28
|
context: IContext;
|
29
|
-
files?:
|
29
|
+
files?: LoadedModel[]
|
30
30
|
}
|
31
31
|
|
32
32
|
export type ContextCallback = (evt: ContextEventArgs) => void | Promise<any> | IComponent;
|
@@ -30,8 +30,8 @@
|
|
30
30
|
import { logHierarchy } from './engine_three_utils.js';
|
31
31
|
import { Time } from './engine_time.js';
|
32
32
|
import { patchTonemapping } from './engine_tonemapping.js';
|
33
|
-
import type { CoroutineData,
|
34
|
-
import { deepClone,delay, DeviceUtilities, getParam } from './engine_utils.js';
|
33
|
+
import type { CoroutineData, ICamera, IComponent, IContext, ILight, LoadedModel, Model, Vec2 } from "./engine_types.js";
|
34
|
+
import { deepClone, delay, DeviceUtilities, getParam } from './engine_utils.js';
|
35
35
|
import type { INeedleXRSessionEventReceiver, NeedleXRSession } from './engine_xr.js';
|
36
36
|
import { NeedleMenu } from './webcomponents/needle menu/needle-menu.js';
|
37
37
|
|
@@ -66,7 +66,7 @@
|
|
66
66
|
/** called on update for each loaded glTF file */
|
67
67
|
onLoadingProgress?: (args: LoadingProgressArgs) => void;
|
68
68
|
/** Called after a gLTF file has finished loading */
|
69
|
-
onLoadingFinished?: (index: number, file: string, glTF:
|
69
|
+
onLoadingFinished?: (index: number, file: string, glTF: Model | null) => void;
|
70
70
|
}
|
71
71
|
|
72
72
|
export class ContextArgs {
|
@@ -855,7 +855,7 @@
|
|
855
855
|
|
856
856
|
// load and create scene
|
857
857
|
let prepare_succeeded = true;
|
858
|
-
let loadedFiles!: Array<
|
858
|
+
let loadedFiles!: Array<LoadedModel | null>;
|
859
859
|
try {
|
860
860
|
Context.Current = this;
|
861
861
|
if (opts) {
|
@@ -1011,8 +1011,8 @@
|
|
1011
1011
|
return true;
|
1012
1012
|
}
|
1013
1013
|
|
1014
|
-
private async internalLoadInitialContent(createId: number, args: ContextCreateArgs): Promise<Array<
|
1015
|
-
const results = new Array<
|
1014
|
+
private async internalLoadInitialContent(createId: number, args: ContextCreateArgs): Promise<Array<LoadedModel>> {
|
1015
|
+
const results = new Array<LoadedModel>();
|
1016
1016
|
// early out if we dont have any files to load
|
1017
1017
|
if (args.files.length === 0) return results;
|
1018
1018
|
|
@@ -1092,7 +1092,7 @@
|
|
1092
1092
|
// It's ok to do this at this point because we know the context has been cleared because the whole `src` attribute has been set
|
1093
1093
|
if (!anyModelFound) {
|
1094
1094
|
for (const res of results) {
|
1095
|
-
if (res && res.file) {
|
1095
|
+
if (res && res.file && "parser" in res.file) {
|
1096
1096
|
let y = 0;
|
1097
1097
|
for (let i = 0; i < res.file.parser.json.materials.length; i++) {
|
1098
1098
|
const mat = await res.file.parser.getDependency("material", i);
|
@@ -2,21 +2,21 @@
|
|
2
2
|
|
3
3
|
import { registerLoader } from "../engine/engine_gltf.js";
|
4
4
|
import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
|
5
|
-
import { VERSION } from "./engine_constants.js";
|
5
|
+
import { PUBLIC_KEY, VERSION } from "./engine_constants.js";
|
6
6
|
import { TonemappingAttributeOptions } from "./engine_element_attributes.js";
|
7
7
|
import { calculateProgress01, EngineLoadingView, type ILoadingViewHandler } from "./engine_element_loading.js";
|
8
8
|
import { arContainerClassName, AROverlayHandler } from "./engine_element_overlay.js";
|
9
9
|
import { hasCommercialLicense } from "./engine_license.js";
|
10
10
|
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "./engine_loaders.js";
|
11
|
-
import {
|
12
|
-
import { Context, ContextCreateArgs } from "./engine_setup.js";
|
13
|
-
import { type INeedleEngineComponent, type
|
11
|
+
import { NeedleLoader } from "./engine_scenetools.js";
|
12
|
+
import { Context, ContextCreateArgs, LoadingProgressArgs } from "./engine_setup.js";
|
13
|
+
import { type INeedleEngineComponent, type LoadedModel } from "./engine_types.js";
|
14
14
|
import { getParam } from "./engine_utils.js";
|
15
15
|
import { ensureFonts } from "./webcomponents/fonts.js";
|
16
16
|
|
17
17
|
//
|
18
18
|
// registering loader here too to make sure it's imported when using engine via vanilla js
|
19
|
-
registerLoader(
|
19
|
+
registerLoader(NeedleLoader);
|
20
20
|
|
21
21
|
const debug = getParam("debugwebcomponent");
|
22
22
|
|
@@ -28,6 +28,9 @@
|
|
28
28
|
const desktopSessionActiveClassName = "desktop-session-active";
|
29
29
|
|
30
30
|
const observedAttributes = [
|
31
|
+
"public-key",
|
32
|
+
"version",
|
33
|
+
|
31
34
|
"hash",
|
32
35
|
"src",
|
33
36
|
"camera-controls",
|
@@ -224,6 +227,9 @@
|
|
224
227
|
console.log("<needle-engine> connected");
|
225
228
|
}
|
226
229
|
|
230
|
+
this.setPublicKey();
|
231
|
+
this.setVersion();
|
232
|
+
|
227
233
|
this.addEventListener("xr-session-started", this.onXRSessionStarted);
|
228
234
|
this.onSetupDesktop();
|
229
235
|
|
@@ -247,10 +253,6 @@
|
|
247
253
|
if (loadId !== this._loadId) return;
|
248
254
|
this.onLoad();
|
249
255
|
}, 1);
|
250
|
-
|
251
|
-
if (VERSION?.length) {
|
252
|
-
this.setAttribute("version", VERSION);
|
253
|
-
}
|
254
256
|
}
|
255
257
|
|
256
258
|
/**
|
@@ -279,11 +281,11 @@
|
|
279
281
|
/**
|
280
282
|
* @internal
|
281
283
|
*/
|
282
|
-
attributeChangedCallback(name: string,
|
283
|
-
if (debug) console.log("attributeChangedCallback", name,
|
284
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
285
|
+
if (debug) console.log("attributeChangedCallback", name, oldValue, newValue);
|
284
286
|
switch (name) {
|
285
287
|
case "src":
|
286
|
-
if (debug) console.warn("<needle-engine src>\nchanged from \"",
|
288
|
+
if (debug) console.warn("<needle-engine src>\nchanged from \"", oldValue, "\" to \"", newValue, "\"")
|
287
289
|
this.onLoad();
|
288
290
|
// this._watcher?.onSourceChanged(newValue);
|
289
291
|
break;
|
@@ -334,6 +336,16 @@
|
|
334
336
|
this.applyAttributes();
|
335
337
|
break;
|
336
338
|
}
|
339
|
+
case "public-key": {
|
340
|
+
if (newValue != PUBLIC_KEY)
|
341
|
+
this.setPublicKey();
|
342
|
+
break;
|
343
|
+
}
|
344
|
+
case "version": {
|
345
|
+
if (newValue != VERSION)
|
346
|
+
this.setVersion();
|
347
|
+
break;
|
348
|
+
}
|
337
349
|
}
|
338
350
|
}
|
339
351
|
|
@@ -426,7 +438,7 @@
|
|
426
438
|
if (debug) console.warn("--------------\nNeedle Engine: Begin loading " + loadId + "\n", filesToLoad);
|
427
439
|
this.onBeforeBeginLoading();
|
428
440
|
|
429
|
-
const loadedFiles: Array<
|
441
|
+
const loadedFiles: Array<LoadedModel> = [];
|
430
442
|
const progressEventDetail = {
|
431
443
|
context: this._context,
|
432
444
|
name: "",
|
@@ -476,10 +488,14 @@
|
|
476
488
|
this.applyAttributes();
|
477
489
|
|
478
490
|
if (debug) console.warn("--------------\nNeedle Engine: finished loading " + loadId + "\n", filesToLoad, `Aborted? ${controller.signal.aborted}`);
|
479
|
-
if (
|
491
|
+
if (controller.signal.aborted) {
|
480
492
|
console.log("Loading finished but aborted...")
|
481
493
|
return;
|
482
494
|
}
|
495
|
+
if (this._loadId !== loadId) {
|
496
|
+
console.log("Load id changed during loading process")
|
497
|
+
return;
|
498
|
+
}
|
483
499
|
|
484
500
|
this._loadingProgress01 = 1;
|
485
501
|
if (useDefaultLoading && success) {
|
@@ -644,6 +660,16 @@
|
|
644
660
|
}
|
645
661
|
}
|
646
662
|
|
663
|
+
private setPublicKey() {
|
664
|
+
if (PUBLIC_KEY && PUBLIC_KEY.length > 0)
|
665
|
+
this.setAttribute("public-key", PUBLIC_KEY);
|
666
|
+
}
|
667
|
+
private setVersion() {
|
668
|
+
if (VERSION && VERSION.length > 0) {
|
669
|
+
this.setAttribute("version", VERSION);
|
670
|
+
}
|
671
|
+
}
|
672
|
+
|
647
673
|
/**
|
648
674
|
* @internal
|
649
675
|
*/
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Bone, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { __internalNotifyObjectDestroyed as __internalRemoveReferences, disposeObjectResources } from "./engine_assetdatabase.js";
|
4
|
-
import {
|
4
|
+
import { ComponentLifecycleEvents } from "./engine_components_internal.js";
|
5
5
|
import { activeInHierarchyFieldName } from "./engine_constants.js";
|
6
6
|
import { editorGuidKeyName } from "./engine_constants.js";
|
7
7
|
import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
|
@@ -480,7 +480,7 @@
|
|
480
480
|
// copy.transform = clone;
|
481
481
|
componentsList.push(copy);
|
482
482
|
objectsMap[comp.guid] = { original: comp, clone: copy };
|
483
|
-
ComponentLifecycleEvents.dispatchComponentLifecycleEvent(
|
483
|
+
ComponentLifecycleEvents.dispatchComponentLifecycleEvent("component-added", copy);
|
484
484
|
}
|
485
485
|
}
|
486
486
|
|
@@ -1,16 +1,14 @@
|
|
1
|
-
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
2
|
-
|
3
1
|
import { SerializationContext } from "./engine_serialization_core.js";
|
4
2
|
import { Context } from "./engine_setup.js";
|
5
|
-
import type { ConstructorConcrete, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
3
|
+
import type { ConstructorConcrete, Model, SourceIdentifier, UIDProvider } from "./engine_types.js";
|
6
4
|
import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
|
7
5
|
|
8
6
|
|
9
7
|
export interface INeedleGltfLoader {
|
10
8
|
createBuiltinComponents(context: Context, gltfId: SourceIdentifier, gltf, seed: number | null | UIDProvider, extension?: NEEDLE_components): Promise<void>
|
11
9
|
writeBuiltinComponentData(comp: object, context: SerializationContext);
|
12
|
-
parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<
|
13
|
-
loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (prog: ProgressEvent) => void): Promise<
|
10
|
+
parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<Model | undefined>;
|
11
|
+
loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (prog: ProgressEvent) => void): Promise<Model | undefined>
|
14
12
|
}
|
15
13
|
|
16
14
|
let gltfLoader: INeedleGltfLoader;
|
@@ -379,11 +379,18 @@
|
|
379
379
|
get mousePositionRC(): Vector2 { return this._pointerPositionsRC[0]; }
|
380
380
|
get mouseDown(): boolean { return this._pointerDown[0]; }
|
381
381
|
get mouseUp(): boolean { return this._pointerUp[0]; }
|
382
|
+
/** Is the primary pointer clicked (usually the left button). This is equivalent to `input.click` */
|
382
383
|
get mouseClick(): boolean { return this._pointerClick[0]; }
|
384
|
+
/** Was a double click detected for the primary pointer? This is equivalent to `input.doubleClick` */
|
383
385
|
get mouseDoubleClick(): boolean { return this._pointerDoubleClick[0]; }
|
384
386
|
get mousePressed(): boolean { return this._pointerPressed[0]; }
|
385
387
|
get mouseWheelChanged(): boolean { return this.getMouseWheelChanged(0); }
|
386
388
|
|
389
|
+
/** Is the primary pointer double clicked (usually the left button). This is equivalent to `input.mouseDoubleClick` */
|
390
|
+
get click() : boolean { return this._pointerClick[0]; }
|
391
|
+
/** Was a double click detected for the primary pointer? */
|
392
|
+
get doubleClick() : boolean { return this._pointerDoubleClick[0]; }
|
393
|
+
|
387
394
|
private _specialCursorTrigger: number = 0;
|
388
395
|
|
389
396
|
setCursorPointer() {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { BUILD_TIME, GENERATOR, VERSION } from "./engine_constants.js";
|
1
|
+
import { BUILD_TIME, GENERATOR, PUBLIC_KEY, VERSION } from "./engine_constants.js";
|
2
2
|
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
3
3
|
import { Context } from "./engine_setup.js";
|
4
4
|
import type { IContext } from "./engine_types.js";
|
@@ -9,7 +9,8 @@
|
|
9
9
|
// This is modified by a bundler (e.g. vite)
|
10
10
|
// Do not edit manually
|
11
11
|
let NEEDLE_ENGINE_LICENSE_TYPE: string = "basic";
|
12
|
-
if (debug)
|
12
|
+
if (debug)
|
13
|
+
console.log("License Type: " + NEEDLE_ENGINE_LICENSE_TYPE)
|
13
14
|
|
14
15
|
/** @internal */
|
15
16
|
export function hasProLicense() {
|
@@ -314,7 +315,8 @@
|
|
314
315
|
// hash: window.location.hash,
|
315
316
|
version: VERSION,
|
316
317
|
generator: GENERATOR,
|
317
|
-
build_time: BUILD_TIME
|
318
|
+
build_time: BUILD_TIME,
|
319
|
+
public_key: PUBLIC_KEY,
|
318
320
|
};
|
319
321
|
const res = navigator.sendBeacon?.(finalUrl, JSON.stringify(beaconData));
|
320
322
|
if (debug) console.log("Send beacon result", res);
|
@@ -1,5 +1,5 @@
|
|
1
1
|
|
2
|
-
import { createLoaders, setDracoDecoderLocation, setKTX2TranscoderLocation } from '@needle-tools/gltf-progressive';
|
2
|
+
import { addDracoAndKTX2Loaders as addDracoAndKTX2LoadersGLTFProgressive, configureLoader, createLoaders, setDracoDecoderLocation, setKTX2TranscoderLocation } from '@needle-tools/gltf-progressive';
|
3
3
|
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
4
4
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
5
5
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
@@ -10,8 +10,6 @@
|
|
10
10
|
|
11
11
|
const debug = getParam("debugdecoders");
|
12
12
|
|
13
|
-
let meshoptDecoder: typeof MeshoptDecoder;
|
14
|
-
|
15
13
|
let loaders: null | { dracoLoader: DRACOLoader, ktx2Loader: KTX2Loader, meshoptDecoder: typeof MeshoptDecoder } = null;
|
16
14
|
|
17
15
|
function ensureLoaders() {
|
@@ -49,8 +47,10 @@
|
|
49
47
|
}
|
50
48
|
|
51
49
|
export function setMeshoptDecoder(_meshoptDecoder: any) {
|
52
|
-
if (_meshoptDecoder !== undefined)
|
53
|
-
|
50
|
+
if (_meshoptDecoder !== undefined) {
|
51
|
+
const loaders = ensureLoaders();
|
52
|
+
loaders.meshoptDecoder = _meshoptDecoder;
|
53
|
+
}
|
54
54
|
}
|
55
55
|
|
56
56
|
/**
|
@@ -60,26 +60,27 @@
|
|
60
60
|
* @returns The GLTFLoader instance with the loaders added.
|
61
61
|
*/
|
62
62
|
export function addDracoAndKTX2Loaders(loader: GLTFLoader, context: Pick<Context, "renderer">) {
|
63
|
-
|
63
|
+
|
64
64
|
const loaders = ensureLoaders();
|
65
65
|
|
66
|
-
if (!meshoptDecoder) {
|
67
|
-
meshoptDecoder = loaders.meshoptDecoder;
|
68
|
-
if (debug) console.log("Using the default meshopt decoder");
|
69
|
-
}
|
70
|
-
|
71
|
-
|
72
66
|
if (context.renderer) {
|
73
67
|
loaders.ktx2Loader.detectSupport(context.renderer);
|
74
68
|
}
|
75
69
|
else
|
76
70
|
console.warn("No renderer provided to detect ktx2 support - loading KTX2 textures will probably fail");
|
77
71
|
|
72
|
+
addDracoAndKTX2LoadersGLTFProgressive(loader);
|
73
|
+
|
78
74
|
if (!loader.dracoLoader)
|
79
75
|
loader.setDRACOLoader(loaders.dracoLoader);
|
80
76
|
if (!(loader as any).ktx2Loader)
|
81
77
|
loader.setKTX2Loader(loaders.ktx2Loader);
|
82
78
|
if (!(loader as any).meshoptDecoder)
|
83
|
-
loader.setMeshoptDecoder(meshoptDecoder);
|
79
|
+
loader.setMeshoptDecoder(loaders.meshoptDecoder);
|
80
|
+
|
81
|
+
configureLoader(loader, {
|
82
|
+
progressive: true,
|
83
|
+
});
|
84
|
+
|
84
85
|
return loader;
|
85
86
|
}
|
@@ -39,7 +39,7 @@
|
|
39
39
|
// private getters: { [key: string]: Function } = {};
|
40
40
|
private hasChanges: boolean = false;
|
41
41
|
private changedProperties: { [key: string]: any } = {};
|
42
|
-
private data = {};
|
42
|
+
// private data = {};
|
43
43
|
|
44
44
|
|
45
45
|
get networkingKey(): string {
|
@@ -74,7 +74,7 @@
|
|
74
74
|
|
75
75
|
notifyChanged(propertyName: string, value: any) {
|
76
76
|
if (this._isReceiving) return;
|
77
|
-
if(debug) console.log("Property changed: " + propertyName, value);
|
77
|
+
if (debug) console.log("Property changed: " + propertyName, value);
|
78
78
|
this.hasChanges = true;
|
79
79
|
this.changedProperties[propertyName] = value;
|
80
80
|
}
|
@@ -88,37 +88,28 @@
|
|
88
88
|
delete this.changedProperties[key];
|
89
89
|
return;
|
90
90
|
}
|
91
|
-
for (const key in this.data) {
|
92
|
-
delete this.data[key];
|
93
|
-
}
|
94
|
-
this.data["guid"] = this.comp.guid;
|
95
91
|
for (const name in this.changedProperties) {
|
92
|
+
const guid = this.comp.guid + "/" + name;
|
96
93
|
const value = this.changedProperties[name];
|
97
|
-
|
98
|
-
|
99
|
-
this.data[name] = value;
|
94
|
+
if (debug) console.log("SEND", this.comp.name, this.networkingKey);
|
95
|
+
net.send(this.networkingKey, { guid, data: value }, SendQueue.Queued);
|
100
96
|
}
|
101
|
-
// console.log("SEND", this.comp.name, this.data, this.networkingKey);
|
102
|
-
net.send(this.networkingKey, this.data, SendQueue.Queued);
|
103
97
|
}
|
104
98
|
|
105
|
-
private onHandleReceiving = (val) => {
|
106
|
-
if (debug)
|
99
|
+
private onHandleReceiving = (val: { guid: string, data: any }) => {
|
100
|
+
if (debug)
|
107
101
|
console.log("RECEIVE", this.comp.name, this.comp.guid, val);
|
108
102
|
if (!this._isInit) return;
|
109
103
|
if (!this.comp) return;
|
110
|
-
|
111
|
-
if (guid
|
104
|
+
// check if this change belongs to this component
|
105
|
+
if (val.guid.startsWith(this.comp.guid) === false) {
|
106
|
+
return;
|
107
|
+
}
|
108
|
+
const [_guid, key] = val.guid.split("/");
|
112
109
|
if (debug) console.log("RECEIVED", this.comp.name, this.comp.guid, val);
|
113
110
|
try {
|
114
111
|
this._isReceiving = true;
|
115
|
-
|
116
|
-
if (key === "guid") continue;
|
117
|
-
// TODO: maybe use serializable here?!
|
118
|
-
const value = val[key];
|
119
|
-
this.comp[key] = value;
|
120
|
-
if(debug) console.log("SET", key, value);
|
121
|
-
}
|
112
|
+
this.comp[key] = val.data;
|
122
113
|
}
|
123
114
|
catch (err) {
|
124
115
|
console.error(err);
|
@@ -210,8 +201,9 @@
|
|
210
201
|
* (for example a networked color is sent as a number and may be converted to a color in the receiver again)
|
211
202
|
* Parameters: (newValue, previousValue)
|
212
203
|
*/
|
213
|
-
export const syncField = function (onFieldChanged
|
204
|
+
export const syncField = function (onFieldChanged: string | FieldChangedCallbackFn | undefined | null = null) {
|
214
205
|
|
206
|
+
|
215
207
|
return function (target: any, _propertyKey: string | { name: string }) {
|
216
208
|
let propertyKey = "";
|
217
209
|
if (typeof _propertyKey === "string") propertyKey = _propertyKey;
|
@@ -226,6 +218,13 @@
|
|
226
218
|
fn = onFieldChanged;
|
227
219
|
}
|
228
220
|
|
221
|
+
if (fn == undefined) {
|
222
|
+
if (isDevEnvironment() || debug) {
|
223
|
+
if (onFieldChanged != undefined)
|
224
|
+
console.warn("syncField: no callback function found for property \"" + propertyKey + "\"", "\"" + onFieldChanged + "\"");
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
229
228
|
const t = target;
|
230
229
|
const internalAwake = t.__internalAwake;
|
231
230
|
if (typeof internalAwake !== "function") {
|
@@ -254,16 +253,17 @@
|
|
254
253
|
this[backingFieldName] = value;
|
255
254
|
// Prevent recursive calls when object is assigned in callback
|
256
255
|
if (invokingCallback) {
|
257
|
-
if (isDevEnvironment())
|
256
|
+
if (isDevEnvironment() || debug)
|
258
257
|
console.warn("Recursive call detected", propertyKey);
|
259
258
|
return;
|
260
259
|
}
|
261
260
|
invokingCallback = true;
|
262
261
|
try {
|
263
262
|
const valueChanged = testValueChanged(value, oldValue);
|
264
|
-
if (debug) console.log("SyncField assignment", propertyKey, "changed?", valueChanged, value);
|
263
|
+
if (debug) console.log("SyncField assignment", propertyKey, "changed?", valueChanged, value, fn);
|
265
264
|
if (valueChanged) {
|
266
|
-
|
265
|
+
const res = fn?.call(this, value, oldValue);
|
266
|
+
if (res !== false) {
|
267
267
|
getSyncer(this)?.notifyChanged(propertyKey, value);
|
268
268
|
}
|
269
269
|
}
|
@@ -2,13 +2,15 @@
|
|
2
2
|
// import { DragControls } from "../engine-components/DragControls.js"
|
3
3
|
// import { ObjectRaycaster } from "../engine-components/ui/Raycaster.js";
|
4
4
|
import { Object3D } from "three";
|
5
|
-
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
6
5
|
|
7
|
-
import type { UIDProvider } from "./engine_types.js";
|
6
|
+
import type { Model, UIDProvider } from "./engine_types.js";
|
8
7
|
// import { Animation } from "../engine-components/Animation.js";
|
9
8
|
|
10
9
|
|
11
|
-
|
10
|
+
/**
|
11
|
+
* @deprecated
|
12
|
+
*/
|
13
|
+
export function onDynamicObjectAdded(_obj: Object3D, _idProv: UIDProvider, _gltf?: Model) {
|
12
14
|
|
13
15
|
// console.warn("Adding components on object has been temporarily disabled");
|
14
16
|
|
@@ -1,16 +1,15 @@
|
|
1
|
-
import { BoxGeometry, BoxHelper, Mesh, MeshBasicMaterial, Object3D, Vector3 } from "three";
|
2
|
-
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
1
|
+
import { BoxGeometry, BoxHelper, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, Vector3, Vector3Like } from "three";
|
3
2
|
|
4
3
|
import { getLoader } from "../engine/engine_gltf.js";
|
5
4
|
import { NetworkConnection } from "../engine/engine_networking.js";
|
6
5
|
import { generateSeed, InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
|
7
6
|
import { Context } from "../engine/engine_setup.js";
|
8
|
-
import * as web from "../engine/engine_web_api.js";
|
9
7
|
import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
|
10
8
|
import { findByGuid } from "./engine_gameobject.js";
|
9
|
+
import { BlobStorage } from "./engine_networking_blob.js";
|
11
10
|
import * as def from "./engine_networking_files_default_components.js"
|
12
11
|
import type { IModel } from "./engine_networking_types.js";
|
13
|
-
import type { IGameObject } from "./engine_types.js";
|
12
|
+
import type { IGameObject, Vec3 } from "./engine_types.js";
|
14
13
|
|
15
14
|
export enum File_Event {
|
16
15
|
File_Spawned = "file-spawned",
|
@@ -22,187 +21,195 @@
|
|
22
21
|
file_hash: string;
|
23
22
|
file_size: number;
|
24
23
|
position: Vector3 | null;
|
24
|
+
scale: Vector3 | null;
|
25
25
|
seed: number;
|
26
26
|
sender: string;
|
27
|
-
|
27
|
+
/** the url to download the file */
|
28
|
+
downloadUrl: string;
|
28
29
|
parentGuid?: string;
|
29
30
|
|
30
31
|
boundsSize?: Vector3;
|
31
32
|
|
32
|
-
constructor(connectionId: string, seed: number, guid: string, name: string, hash: string, size: number, position: Vector3
|
33
|
+
constructor(connectionId: string, seed: number, guid: string, name: string, hash: string, size: number, position: Vector3, scale: Vector3, downloadUrl: string) {
|
33
34
|
this.seed = seed;
|
34
35
|
this.guid = guid;
|
35
36
|
this.file_name = name;
|
36
37
|
this.file_hash = hash;
|
37
38
|
this.file_size = size;
|
38
39
|
this.position = position;
|
40
|
+
this.scale = scale;
|
39
41
|
this.sender = connectionId;
|
40
|
-
this.
|
42
|
+
this.downloadUrl = downloadUrl;
|
41
43
|
}
|
42
44
|
}
|
43
45
|
|
44
46
|
|
45
|
-
export async function addFile(file: File, context: Context, backendUrl?: string): Promise<GLTF | null> {
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
const reader = new FileReader()
|
51
|
-
reader.readAsArrayBuffer(file);
|
52
|
-
reader.onloadend = async (_ev: ProgressEvent<FileReader>) => {
|
53
|
-
const content = reader.result as ArrayBuffer;
|
54
|
-
// first load it locally
|
55
|
-
const seed = generateSeed();
|
56
|
-
const prov = new InstantiateIdProvider(seed);
|
57
|
-
const gltf: GLTF = await getLoader().parseSync(context, content, file.name, prov) as GLTF;
|
58
|
-
if (gltf && gltf.scene) {
|
59
|
-
const obj = gltf.scene as unknown as IGameObject;
|
60
|
-
// if we dont have a guid yet (because components guids are actually created in a callback a bit later)
|
61
|
-
// we just use the same seed and generate a guid for the root only
|
62
|
-
// this should be the exact same guid the instantiate call will produce
|
63
|
-
if (!obj.guid) {
|
64
|
-
const prov = new InstantiateIdProvider(seed);
|
65
|
-
obj.guid = prov.generateUUID();
|
66
|
-
}
|
67
|
-
if (backendUrl)
|
68
|
-
handleUpload(context.connection, file, seed, obj, backendUrl);
|
69
|
-
def.onDynamicObjectAdded(obj, prov, gltf);
|
70
|
-
resolve(gltf);
|
71
|
-
}
|
72
|
-
};
|
73
|
-
});
|
74
|
-
}
|
75
|
-
else {
|
76
|
-
console.warn("Unsupported file type: " + name, file)
|
77
|
-
}
|
48
|
+
// ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => {
|
49
|
+
// beginListenFileSpawn(evt.context as Context);
|
50
|
+
// })
|
78
51
|
|
79
|
-
|
80
|
-
|
52
|
+
// function beginListenFileSpawn(context: Context) {
|
53
|
+
// context.connection.beginListen(File_Event.File_Spawned, async (evt: FileSpawnModel) => {
|
54
|
+
// if (evt.sender !== context.connection.connectionId) {
|
55
|
+
// console.log("received file event", evt);
|
56
|
+
// const prev = addPreview(evt, context);
|
57
|
+
// let bin: ArrayBuffer | null = null;
|
58
|
+
// try {
|
59
|
+
// const arr = await BlobStorage.download(evt.downloadUrl, prog => {
|
60
|
+
// prev.onProgress(prog.loaded / prog.total);
|
61
|
+
// });
|
62
|
+
// if (arr)
|
63
|
+
// bin = arr.buffer;
|
64
|
+
// }
|
65
|
+
// finally {
|
66
|
+
// removePreview(evt, context);
|
67
|
+
// }
|
68
|
+
// if (bin) {
|
69
|
+
// const prov = new InstantiateIdProvider(evt.seed);
|
70
|
+
// const gltf = await getLoader().parseSync(context, bin, evt.file_name, prov);
|
71
|
+
// if (gltf && gltf.scene) {
|
72
|
+
// const obj = gltf.scene;
|
73
|
+
// def.onDynamicObjectAdded(obj, prov, gltf);
|
74
|
+
// // if we process new scripts immediately references that rely on guids are not properly resolved
|
75
|
+
// // for example duplicatable "object" reference will not be found anymore because guid has changed
|
76
|
+
// // processNewScripts(context);
|
81
77
|
|
82
|
-
|
78
|
+
// // add object to proper parent
|
79
|
+
// if (evt.parentGuid) {
|
80
|
+
// const parent = findByGuid(evt.parentGuid, context.scene) as Object3D;
|
81
|
+
// if (parent && "add" in parent) parent.add(obj);
|
82
|
+
// }
|
83
|
+
// if (!obj.parent)
|
84
|
+
// context.scene.add(obj);
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
resolve(gltf);
|
94
|
-
}
|
95
|
-
else {
|
96
|
-
console.warn("Unsupported file type: " + url.toString());
|
97
|
-
}
|
98
|
-
});
|
99
|
-
}
|
86
|
+
// if (evt.position !== null) {
|
87
|
+
// obj.position.copy(evt.position);
|
88
|
+
// }
|
89
|
+
// }
|
90
|
+
// }
|
91
|
+
// else console.error("download didnt return file");
|
92
|
+
// }
|
93
|
+
// });
|
94
|
+
// }
|
100
95
|
|
101
96
|
|
102
|
-
ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => {
|
103
|
-
beginListenFileSpawn(evt.context as Context);
|
104
|
-
})
|
105
97
|
|
98
|
+
// async function handleUpload(connection: NetworkConnection, file: File, seed: number, obj: IGameObject) {
|
99
|
+
// if (!connection.connectionId) {
|
100
|
+
// console.error("Can not upload file: not connected to networking backend");
|
101
|
+
// return;
|
102
|
+
// }
|
103
|
+
// if (!obj.guid) {
|
104
|
+
// console.error("Can not upload file: the object does not have a guid");
|
105
|
+
// return;
|
106
|
+
// }
|
107
|
+
// // then try uploading it
|
108
|
+
// const upload_result = await BlobStorage.upload(file);
|
109
|
+
// if (!upload_result) {
|
110
|
+
// return;
|
111
|
+
// }
|
112
|
+
// if (!upload_result.filename) {
|
113
|
+
// console.error("Can not send upload event - no filename", file.name);
|
114
|
+
// return;
|
115
|
+
// }
|
116
|
+
// if (!upload_result.hash) {
|
117
|
+
// console.error("Can not send upload event - no hash", file.name);
|
118
|
+
// return;
|
119
|
|