@needle-tools/materialx 1.7.0 → 1.7.1

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 CHANGED
@@ -4,6 +4,13 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.7.1] - 2026-07-01
8
+
9
+ ### Fixed
10
+ - Normalized MaterialX texture request paths when combining loader search paths, `fileprefix`, and relative filename values, avoiding duplicate slashes in hosted asset URLs.
11
+ - Preserved absolute texture URLs and filename substitution tokens while resolving relative MaterialX texture paths.
12
+ - Added MaterialX path fixtures with reference renders for included libraries, file prefixes, relative paths, and texture-load diagnostics.
13
+
7
14
  ## [1.7.0] - 2026-06-30
8
15
 
9
16
  ### Added
package/README.md CHANGED
@@ -136,6 +136,10 @@ globalThis.NEEDLE_MATERIALX_LOCATION = "/assets/materialx/";
136
136
 
137
137
  IMPORTANT: The value must end with a trailing slash (`/`).
138
138
 
139
+ ### Texture Path Resolution
140
+
141
+ MaterialX texture filenames are resolved from the MaterialX-authored filename value, any active `fileprefix`, and the loader search path. Relative paths are normalized for web requests so hosted asset URLs do not gain duplicate slashes, while absolute URLs such as `https://`, `file://`, root-relative paths, data URIs, blob URLs, and MaterialX filename tokens such as `<UDIM>` are preserved.
142
+
139
143
 
140
144
  ## glTF Extension: NEEDLE_materials_mtlx
141
145
 
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.1",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
7
7
  "main": "index.js",
@@ -122,6 +122,75 @@ function addToCache(key, value) {
122
122
  if (debug) console.log('[MaterialX] Added to cache:', key, value);
123
123
  }
124
124
 
125
+ /**
126
+ * @param {string} value
127
+ * @returns {boolean}
128
+ */
129
+ function isAbsoluteTexturePath(value) {
130
+ return /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//") || value.startsWith("/");
131
+ }
132
+
133
+ /**
134
+ * @param {string} value
135
+ * @returns {boolean}
136
+ */
137
+ function isAbsoluteUrl(value) {
138
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
139
+ }
140
+
141
+ /**
142
+ * @param {string} value
143
+ * @returns {string}
144
+ */
145
+ function withTrailingSlash(value) {
146
+ return value.endsWith(IMAGE_PATH_SEPARATOR) ? value : `${value}${IMAGE_PATH_SEPARATOR}`;
147
+ }
148
+
149
+ /**
150
+ * @param {string} value
151
+ * @returns {string}
152
+ */
153
+ function normalizeRelativeTexturePath(value) {
154
+ const suffixStart = value.search(/[?#]/);
155
+ const path = suffixStart >= 0 ? value.slice(0, suffixStart) : value;
156
+ const suffix = suffixStart >= 0 ? value.slice(suffixStart) : "";
157
+ const hasLeadingSlash = path.startsWith(IMAGE_PATH_SEPARATOR);
158
+ const parts = [];
159
+
160
+ for (const part of path.split(IMAGE_PATH_SEPARATOR)) {
161
+ if (!part || part === ".") continue;
162
+ if (part === "..") {
163
+ const previous = parts[parts.length - 1];
164
+ if (previous && previous !== "..") parts.pop();
165
+ else if (!hasLeadingSlash) parts.push(part);
166
+ continue;
167
+ }
168
+ parts.push(part);
169
+ }
170
+
171
+ const normalizedPath = `${hasLeadingSlash ? IMAGE_PATH_SEPARATOR : ""}${parts.join(IMAGE_PATH_SEPARATOR)}`;
172
+ return `${normalizedPath}${suffix}`;
173
+ }
174
+
175
+ /**
176
+ * @param {string} searchPath
177
+ * @param {string} value
178
+ * @returns {string}
179
+ */
180
+ function resolveTexturePath(searchPath, value) {
181
+ if (isAbsoluteTexturePath(value)) return value;
182
+ if (!searchPath) return value;
183
+
184
+ const normalizedSearchPath = searchPath.replace(/\/+$/, "");
185
+ if (!normalizedSearchPath) return value;
186
+
187
+ if (isAbsoluteUrl(normalizedSearchPath)) {
188
+ return new URL(value, withTrailingSlash(normalizedSearchPath)).href;
189
+ }
190
+
191
+ return normalizeRelativeTexturePath(`${normalizedSearchPath}${IMAGE_PATH_SEPARATOR}${value}`);
192
+ }
193
+
125
194
  /**
126
195
  * Get Three uniform from MaterialX value
127
196
  * @param {any} uniforms
@@ -165,19 +234,19 @@ function toThreeUniform(threeUniforms, uniforms, type, value, name, loaders, sea
165
234
  // Cache / reuse texture to avoid reload overhead.
166
235
  // Note: that data blobs and embedded data textures are not cached as they are transient data.
167
236
  let checkCache = true;
168
- let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
237
+ let texturePath = value;
169
238
  if (value.startsWith('blob:')) {
170
- texturePath = value;
171
239
  checkCache = false;
172
240
  }
173
241
  else if (value.startsWith('data:')) {
174
- texturePath = value;
175
242
  checkCache = false;
176
243
  }
177
244
  else if (value.startsWith('http')) {
178
- texturePath = value;
179
245
  checkCache = true;
180
246
  }
247
+ else {
248
+ texturePath = resolveTexturePath(searchPath, value);
249
+ }
181
250
 
182
251
  const cacheKey = loaders.cacheKey?.length ? `${loaders.cacheKey}-${texturePath}` : texturePath;
183
252
  const cacheValue = checkCache ? tryGetFromCache(cacheKey) : null;
@@ -247,7 +316,7 @@ function toThreeUniform(threeUniforms, uniforms, type, value, name, loaders, sea
247
316
  case 'displacementshader':
248
317
  case 'volumeshader':
249
318
  case 'lightshader':
250
- // MaterialX closure/shader types — not real uniforms, skip silently
319
+ // MaterialX closure/shader types — not emitted uniforms, skip silently
251
320
  break;
252
321
  default:
253
322
  const key = type + ':' + name;