@needle-tools/materialx 1.7.0-next.8333b4d → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -2
- package/README.md +25 -1
- package/bin/JsMaterialXCore.js +1 -1
- package/bin/JsMaterialXGenShader.js +1 -1
- package/package.json +6 -3
- package/src/constants.js +4 -4
- package/src/loader/loader.three.d.ts +2 -0
- package/src/loader/loader.three.js +1 -0
- package/src/materialx.helper.js +15 -4
- package/src/materialx.js +19 -1
- package/src/materialx.material.d.ts +2 -0
- package/src/materialx.material.js +84 -3
- package/src/utils.js +16 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/materialx",
|
|
3
3
|
"description": "MaterialX material support for three.js and Needle Engine – render physically based MaterialX shaders in the browser via WebAssembly",
|
|
4
|
-
"version": "1.7.0
|
|
4
|
+
"version": "1.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
7
7
|
"main": "index.js",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"three": ">=0.160.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --import=./tests/unit/register-json-loader.js --test tests/unit/**/*.test.js"
|
|
30
|
+
"test": "node --import=./tests/unit/register-json-loader.js --test tests/unit/**/*.test.js",
|
|
31
|
+
"test:runtime:node": "node tests/runtime/materialx-runtime-smoke.mjs",
|
|
32
|
+
"test:runtime:deno": "deno run --allow-read --no-lock --node-modules-dir=auto tests/runtime/materialx-runtime-smoke.mjs",
|
|
33
|
+
"test:runtime:bun": "bun tests/runtime/materialx-runtime-smoke.mjs"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@needle-tools/engine": "4.x",
|
|
@@ -75,4 +78,4 @@
|
|
|
75
78
|
"type": "git",
|
|
76
79
|
"url": "https://github.com/needle-tools/needle-engine-materialx.git"
|
|
77
80
|
}
|
|
78
|
-
}
|
|
81
|
+
}
|
package/src/constants.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import pkg from '../package.json';
|
|
1
|
+
// Keep the runtime version tied to package.json. The import attribute is
|
|
2
|
+
// required by browsers and Node.js for JSON modules; bundlers also understand
|
|
3
|
+
// this shape.
|
|
4
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
5
5
|
export const VERSION = pkg.version;
|
|
@@ -44,6 +44,8 @@ export interface MaterialXLoaderOptions {
|
|
|
44
44
|
environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
|
|
45
45
|
/** Match Three.js glossy specular antialiasing. Defaults to true. */
|
|
46
46
|
specularAntialiasing?: boolean;
|
|
47
|
+
/** Generate missing MikkTSpace tangents for tangent-dependent shaders. Defaults to true. */
|
|
48
|
+
generateTangents?: boolean;
|
|
47
49
|
/** Flip texcoord V at the MaterialX texcoord node output. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
|
|
48
50
|
hwTexcoordVerticalFlip?: boolean;
|
|
49
51
|
/** Flip texcoord V inside MaterialX file texture sampling. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
|
|
@@ -358,6 +358,7 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
|
|
|
358
358
|
context: context || {},
|
|
359
359
|
environmentRadianceMode: options?.environmentRadianceMode,
|
|
360
360
|
specularAntialiasing: options?.specularAntialiasing,
|
|
361
|
+
generateTangents: options?.generateTangents,
|
|
361
362
|
parameters: {
|
|
362
363
|
// MASK uses discard; BLEND uses Three.js transparent sorting and blending.
|
|
363
364
|
transparent: renderAsTransparent,
|
package/src/materialx.helper.js
CHANGED
|
@@ -71,10 +71,22 @@ function fromMatrix(matrix, dimension) {
|
|
|
71
71
|
* @property {(path: string) => Promise<THREE.Texture | null | void>} getTexture - Get a texture by path
|
|
72
72
|
*/
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} src
|
|
76
|
+
* @returns {HTMLImageElement | { src: string }}
|
|
77
|
+
*/
|
|
78
|
+
function createPortableImage(src) {
|
|
79
|
+
if (typeof Image !== 'undefined') {
|
|
80
|
+
const image = new Image();
|
|
81
|
+
image.src = src;
|
|
82
|
+
return image;
|
|
83
|
+
}
|
|
84
|
+
return { src };
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
const defaultTexture = new THREE.Texture();
|
|
75
88
|
defaultTexture.needsUpdate = true;
|
|
76
|
-
defaultTexture.image =
|
|
77
|
-
defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
|
|
89
|
+
defaultTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg==");
|
|
78
90
|
// defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAB5QTFRFAAAABAQEw8PD////v7+/vb29Xl5eQEBA+/v7PDw8GPBYkgAAAB1JREFUeJxjZGBgYFQSABIUMlxgDGMGBtaIAnIZAKwQCSDYUEZEAAAAAElFTkSuQmCC";
|
|
79
91
|
// defaultTexture.wrapS = THREE.RepeatWrapping;
|
|
80
92
|
// defaultTexture.wrapT = THREE.RepeatWrapping;
|
|
@@ -84,8 +96,7 @@ defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAA
|
|
|
84
96
|
|
|
85
97
|
const defaultNormalTexture = new THREE.Texture();
|
|
86
98
|
defaultNormalTexture.needsUpdate = true;
|
|
87
|
-
defaultNormalTexture.image =
|
|
88
|
-
defaultNormalTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=";
|
|
99
|
+
defaultNormalTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=");
|
|
89
100
|
|
|
90
101
|
/**
|
|
91
102
|
* @param {string} key
|
package/src/materialx.js
CHANGED
|
@@ -17,6 +17,21 @@ function isTextureLike(value) {
|
|
|
17
17
|
return !!value && typeof value === "object" && /** @type {{ isTexture?: unknown }} */(value).isTexture === true;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function isNodeRuntime() {
|
|
21
|
+
return !!globalThis.process?.versions?.node && globalThis.process?.type !== "renderer";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getNodePackageAssetPaths() {
|
|
25
|
+
const nodeUrlModule = "node:url";
|
|
26
|
+
const { fileURLToPath } = await import(/* @vite-ignore */ nodeUrlModule);
|
|
27
|
+
const binDirectory = ["..", "bin"].join("/") + "/";
|
|
28
|
+
return [
|
|
29
|
+
"JsMaterialXCore.wasm",
|
|
30
|
+
"JsMaterialXGenShader.wasm",
|
|
31
|
+
"JsMaterialXGenShader.data.txt",
|
|
32
|
+
].map(fileName => fileURLToPath(new URL(binDirectory + fileName, import.meta.url)));
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
|
|
21
36
|
/**
|
|
22
37
|
* Preloads the MaterialX WebAssembly module.
|
|
@@ -69,7 +84,10 @@ export async function ready() {
|
|
|
69
84
|
|
|
70
85
|
const location = globalThis.NEEDLE_MATERIALX_LOCATION;
|
|
71
86
|
|
|
72
|
-
if (
|
|
87
|
+
if (isNodeRuntime()) {
|
|
88
|
+
urls = await getNodePackageAssetPaths();
|
|
89
|
+
}
|
|
90
|
+
else if (location === "package" || location === "bin/" || location === "./bin/" || location === "../bin/") {
|
|
73
91
|
// Use local files from the @needle-tools/materialx npm package.
|
|
74
92
|
// Vite's ?url suffix copies these files to the output directory
|
|
75
93
|
// and returns their URL automatically — no CDN download needed.
|
|
@@ -12,6 +12,7 @@ declare type MaterialXMaterialInitParameters = {
|
|
|
12
12
|
parameters?: MaterialParameters;
|
|
13
13
|
environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
|
|
14
14
|
specularAntialiasing?: boolean;
|
|
15
|
+
generateTangents?: boolean;
|
|
15
16
|
debug?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -38,6 +39,7 @@ export declare class MaterialXMaterial extends ShaderMaterial {
|
|
|
38
39
|
envMapRotation: Euler;
|
|
39
40
|
environmentRadianceMode: MaterialXEnvironmentRadianceMode;
|
|
40
41
|
specularAntialiasing: boolean;
|
|
42
|
+
generateTangents: boolean;
|
|
41
43
|
updateUniforms(environment: MaterialXEnvironment, _renderer: WebGLRenderer, object: Object3D, camera: Camera, time?: number, frame?: number): void;
|
|
42
44
|
|
|
43
45
|
private updateEnvironmentUniforms(environment: MaterialXEnvironment, scene: Scene): void;
|
|
@@ -5,6 +5,11 @@ import { generateMaterialPropertiesForUniforms, getUniformValues, getLightTypeId
|
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_ENVIRONMENT_RADIANCE_MODE = "three-pmrem";
|
|
7
7
|
|
|
8
|
+
/** @type {Promise<{ computeMikkTSpaceTangents: Function, MikkTSpace: any }> | null} */
|
|
9
|
+
let mikkTSpaceTangentsPromise = null;
|
|
10
|
+
/** @type {WeakMap<BufferGeometry, Promise<boolean>>} */
|
|
11
|
+
const pendingTangentGenerations = new WeakMap();
|
|
12
|
+
|
|
8
13
|
// Add helper matrices for uniform updates (similar to MaterialX example)
|
|
9
14
|
const worldInverseMat = new Matrix4();
|
|
10
15
|
const worldTransposeMat = new Matrix4();
|
|
@@ -91,6 +96,60 @@ function createThreeLightUniforms() {
|
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
function getMikkTSpaceTangents() {
|
|
100
|
+
mikkTSpaceTangentsPromise ??= Promise.all([
|
|
101
|
+
import('three/examples/jsm/utils/BufferGeometryUtils.js'),
|
|
102
|
+
import('three/examples/jsm/libs/mikktspace.module.js'),
|
|
103
|
+
]).then(async ([utils, MikkTSpace]) => {
|
|
104
|
+
await MikkTSpace.ready;
|
|
105
|
+
return {
|
|
106
|
+
computeMikkTSpaceTangents: utils.computeMikkTSpaceTangents,
|
|
107
|
+
MikkTSpace,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
return mikkTSpaceTangentsPromise;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {BufferGeometry} geometry
|
|
115
|
+
* @returns {Promise<boolean>}
|
|
116
|
+
*/
|
|
117
|
+
function ensureGeometryTangents(geometry) {
|
|
118
|
+
if (geometry.attributes.tangent) return Promise.resolve(true);
|
|
119
|
+
|
|
120
|
+
let pending = pendingTangentGenerations.get(geometry);
|
|
121
|
+
if (pending) return pending;
|
|
122
|
+
|
|
123
|
+
pending = (async () => {
|
|
124
|
+
if (!geometry.attributes.position || !geometry.attributes.uv) {
|
|
125
|
+
console.warn('[MaterialX] Cannot generate tangents: geometry requires position and uv attributes.');
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!geometry.attributes.normal) {
|
|
130
|
+
geometry.computeVertexNormals();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { computeMikkTSpaceTangents, MikkTSpace } = await getMikkTSpaceTangents();
|
|
134
|
+
computeMikkTSpaceTangents(geometry, MikkTSpace);
|
|
135
|
+
|
|
136
|
+
if (geometry.attributes.tangent) {
|
|
137
|
+
geometry.attributes.tangent.needsUpdate = true;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
})().catch(error => {
|
|
143
|
+
console.warn('[MaterialX] Failed to generate MikkTSpace tangents.', error);
|
|
144
|
+
return false;
|
|
145
|
+
}).finally(() => {
|
|
146
|
+
pendingTangentGenerations.delete(geometry);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
pendingTangentGenerations.set(geometry, pending);
|
|
150
|
+
return pending;
|
|
151
|
+
}
|
|
152
|
+
|
|
94
153
|
const CUBE_UV_REFLECTION_FUNCTIONS = `
|
|
95
154
|
float mx_cubeuv_getFace(vec3 direction) {
|
|
96
155
|
vec3 absDirection = abs(direction);
|
|
@@ -305,6 +364,7 @@ function patchImageAddressModes(fragmentShader) {
|
|
|
305
364
|
* @property {import('three').MaterialParameters} [parameters] - Optional parameters
|
|
306
365
|
* @property {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} [environmentRadianceMode]
|
|
307
366
|
* @property {boolean} [specularAntialiasing] - Match Three.js glossy specular antialiasing. Defaults to true.
|
|
367
|
+
* @property {boolean} [generateTangents] - Generate missing MikkTSpace tangents for tangent-dependent shaders. Defaults to true.
|
|
308
368
|
* @property {boolean} [debug] - Debug flag
|
|
309
369
|
*/
|
|
310
370
|
|
|
@@ -336,6 +396,7 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
336
396
|
this.envMapRotation.copy(source.envMapRotation);
|
|
337
397
|
this.environmentRadianceMode = source.environmentRadianceMode;
|
|
338
398
|
this.specularAntialiasing = source.specularAntialiasing;
|
|
399
|
+
this.generateTangents = source.generateTangents;
|
|
339
400
|
this.ready = source.ready;
|
|
340
401
|
generateMaterialPropertiesForUniforms(this, this._shader.getStage('pixel'));
|
|
341
402
|
generateMaterialPropertiesForUniforms(this, this._shader.getStage('vertex'));
|
|
@@ -349,6 +410,8 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
349
410
|
_shader = null;
|
|
350
411
|
/** @type {boolean} */
|
|
351
412
|
_needsTangents = false;
|
|
413
|
+
/** @type {WeakSet<BufferGeometry>} */
|
|
414
|
+
_pendingTangentGeometries = new WeakSet();
|
|
352
415
|
/** @type {Promise<void>} */
|
|
353
416
|
ready = Promise.resolve();
|
|
354
417
|
|
|
@@ -368,6 +431,7 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
368
431
|
/** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
|
|
369
432
|
let environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
|
|
370
433
|
let specularAntialiasing = true;
|
|
434
|
+
let generateTangents = true;
|
|
371
435
|
|
|
372
436
|
if (init) {
|
|
373
437
|
|
|
@@ -423,6 +487,7 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
423
487
|
// the same way MeshStandardMaterial does.
|
|
424
488
|
fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
|
|
425
489
|
specularAntialiasing = init.specularAntialiasing ?? true;
|
|
490
|
+
generateTangents = init.generateTangents ?? true;
|
|
426
491
|
if (specularAntialiasing) {
|
|
427
492
|
fragmentShader = patchEnvironmentSpecularAntialiasing(fragmentShader);
|
|
428
493
|
}
|
|
@@ -698,6 +763,7 @@ $2`
|
|
|
698
763
|
this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
|
|
699
764
|
this.environmentRadianceMode = environmentRadianceMode;
|
|
700
765
|
this.specularAntialiasing = specularAntialiasing;
|
|
766
|
+
this.generateTangents = generateTangents;
|
|
701
767
|
|
|
702
768
|
Object.assign(this.uniforms,
|
|
703
769
|
// Three.js light uniforms (required when lights: true). These use
|
|
@@ -760,10 +826,23 @@ $2`
|
|
|
760
826
|
*/
|
|
761
827
|
onBeforeRender(renderer, _scene, camera, geometry, object, _group) {
|
|
762
828
|
if (this._needsTangents && !geometry.attributes.tangent) {
|
|
763
|
-
if (
|
|
829
|
+
if (this.generateTangents) {
|
|
830
|
+
if (!this._pendingTangentGeometries.has(geometry)) {
|
|
831
|
+
this._pendingTangentGeometries.add(geometry);
|
|
832
|
+
ensureGeometryTangents(geometry).then(generated => {
|
|
833
|
+
if (generated) {
|
|
834
|
+
this.needsUpdate = true;
|
|
835
|
+
} else if (!this._missingTangentsWarned) {
|
|
836
|
+
this._missingTangentsWarned = true;
|
|
837
|
+
console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but could not be generated for the geometry.`);
|
|
838
|
+
}
|
|
839
|
+
}).finally(() => {
|
|
840
|
+
this._pendingTangentGeometries.delete(geometry);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
} else if (!this._missingTangentsWarned) {
|
|
764
844
|
this._missingTangentsWarned = true;
|
|
765
|
-
console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry.`);
|
|
766
|
-
// TODO: can we compute tangents here?
|
|
845
|
+
console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry. Automatic tangent generation is disabled.`);
|
|
767
846
|
}
|
|
768
847
|
}
|
|
769
848
|
const time = this._context?.getTime?.() || getTime();
|
|
@@ -786,6 +865,8 @@ $2`
|
|
|
786
865
|
environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
|
|
787
866
|
/** @type {boolean} */
|
|
788
867
|
specularAntialiasing = true;
|
|
868
|
+
/** @type {boolean} */
|
|
869
|
+
generateTangents = true;
|
|
789
870
|
|
|
790
871
|
/**
|
|
791
872
|
* @param {import("three").WebGLRenderer} _renderer
|
package/src/utils.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* @returns {boolean|string} Parameter value or false if not found
|
|
8
8
|
*/
|
|
9
9
|
export function getParam(name) {
|
|
10
|
-
const
|
|
10
|
+
const search = globalThis.location?.search ?? globalThis.window?.location?.search;
|
|
11
|
+
if (!search) return false;
|
|
12
|
+
const urlParams = new URLSearchParams(search);
|
|
11
13
|
const param = urlParams.get(name);
|
|
12
14
|
if (param == null || param === "0" || param === "false") return false;
|
|
13
15
|
if (param === "") return true;
|
|
@@ -35,22 +37,26 @@ export function getFrame() {
|
|
|
35
37
|
return frame;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const
|
|
40
|
+
const performanceApi = globalThis.performance
|
|
41
|
+
|| globalThis.window?.performance
|
|
42
|
+
|| /** @type {any} */ (globalThis.window)?.webkitPerformance
|
|
43
|
+
|| /** @type {any} */ (globalThis.window)?.mozPerformance
|
|
44
|
+
|| { now: () => Date.now() };
|
|
39
45
|
|
|
40
46
|
function updateTime() {
|
|
41
|
-
time =
|
|
47
|
+
time = performanceApi.now() / 1000; // Convert to seconds
|
|
42
48
|
frame++;
|
|
43
|
-
|
|
49
|
+
globalThis.requestAnimationFrame?.(updateTime);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
globalThis.requestAnimationFrame?.(updateTime);
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
export async function waitForNetworkIdle() {
|
|
52
|
-
if (typeof requestIdleCallback !== "undefined") {
|
|
53
|
-
return new Promise(res => requestIdleCallback(res));
|
|
58
|
+
if (typeof globalThis.requestIdleCallback !== "undefined") {
|
|
59
|
+
return new Promise(res => globalThis.requestIdleCallback(res));
|
|
54
60
|
}
|
|
55
61
|
else {
|
|
56
62
|
console.debug("[MaterialX] Can not wait for network idle, using fallback");
|
|
@@ -61,5 +67,6 @@ export async function waitForNetworkIdle() {
|
|
|
61
67
|
|
|
62
68
|
export function isDevEnvironment() {
|
|
63
69
|
// check if we're in localhost or using an ip address
|
|
64
|
-
|
|
65
|
-
}
|
|
70
|
+
const hostname = globalThis.location?.hostname ?? globalThis.window?.location?.hostname;
|
|
71
|
+
return hostname === "localhost" || !!hostname && /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
|
72
|
+
}
|