@openusd-wasm/utils 0.0.2
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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/openusd_utils.d.ts +39 -0
- package/dist/openusd_utils.js +466 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 KeJun <https://github.com/kejunmao>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @openusd-wasm/utils
|
|
2
|
+
|
|
3
|
+
Shared utilities for browser and worker OpenUSD workflows backed by
|
|
4
|
+
`@openusd-wasm/pxr`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { toLayerRelativeAssetPath } from '@openusd-wasm/utils'
|
|
8
|
+
|
|
9
|
+
refs.AddReference(toLayerRelativeAssetPath('assets/gearbox.usdz'), '')
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Asset References
|
|
13
|
+
|
|
14
|
+
When generating USDA files that reference assets written next to the layer,
|
|
15
|
+
author explicit layer-relative asset paths. OpenUSD treats
|
|
16
|
+
`@assets/model.usdz@` as a search-path asset, while `@./assets/model.usdz@`
|
|
17
|
+
is anchored to the current layer directory and packages without transient
|
|
18
|
+
`0/model.usdz` remap warnings.
|
|
19
|
+
|
|
20
|
+
## Asset Resolution
|
|
21
|
+
|
|
22
|
+
`autoResolveAssetFiles` and `autoResolveTextureFiles` scan USD references and
|
|
23
|
+
metadata, fetch same-origin relative assets, and return virtual filesystem
|
|
24
|
+
entries that can be written next to the root layer before opening or packaging
|
|
25
|
+
with OpenUSD.
|
|
26
|
+
|
|
27
|
+
`findPackageRootLayer`, `createPackageTextureResolver`, and
|
|
28
|
+
`createTextureResolverFromEntries` help inspect stored USDZ packages and map
|
|
29
|
+
packaged image entries to object URLs.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Pxr } from "@openusd-wasm/pxr";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface USDAssetResolverOptions {
|
|
5
|
+
sourcePath?: string;
|
|
6
|
+
fileName?: string;
|
|
7
|
+
autoResolveAssets?: boolean;
|
|
8
|
+
assetSearchExtensions?: string[];
|
|
9
|
+
assetSearchRoots?: string[];
|
|
10
|
+
maxAssetReferences?: number;
|
|
11
|
+
maxAssetReferenceDepth?: number;
|
|
12
|
+
}
|
|
13
|
+
interface USDAssetValue {
|
|
14
|
+
path?: string;
|
|
15
|
+
resolvedPath?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
}
|
|
18
|
+
interface USDTextureResolverContext<TMaterial = unknown, TShader = unknown> {
|
|
19
|
+
sourcePath: string;
|
|
20
|
+
material?: TMaterial;
|
|
21
|
+
shader?: TShader;
|
|
22
|
+
}
|
|
23
|
+
type USDTextureResolver<TMaterial = unknown, TShader = unknown> = (asset: USDAssetValue, context: USDTextureResolverContext<TMaterial, TShader>) => string | null | undefined;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/asset-resolver.d.ts
|
|
26
|
+
declare function toLayerRelativeAssetPath(assetPath: string): string;
|
|
27
|
+
declare function autoResolveAssetFiles(pxr: Pxr, rootFilePath: string, options: USDAssetResolverOptions): Promise<Record<string, Uint8Array>>;
|
|
28
|
+
declare function autoResolveTextureFiles(metadata: unknown, existingFiles: Record<string, Uint8Array>, options: USDAssetResolverOptions): Promise<Record<string, Uint8Array>>;
|
|
29
|
+
declare function createTextureResolverFromEntries(entries: Map<string, Uint8Array>): {
|
|
30
|
+
resolve: USDTextureResolver;
|
|
31
|
+
urls: Map<string, string>;
|
|
32
|
+
} | null;
|
|
33
|
+
declare function createPackageTextureResolver(data: Uint8Array): {
|
|
34
|
+
resolve: USDTextureResolver;
|
|
35
|
+
urls: Map<string, string>;
|
|
36
|
+
} | null;
|
|
37
|
+
declare function findPackageRootLayer(data: Uint8Array): string | null;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { type USDAssetResolverOptions, type USDAssetValue, type USDTextureResolver, type USDTextureResolverContext, autoResolveAssetFiles, autoResolveTextureFiles, createPackageTextureResolver, createTextureResolverFromEntries, findPackageRootLayer, toLayerRelativeAssetPath };
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
//#region src/asset-resolver.ts
|
|
2
|
+
const DEFAULT_ASSET_SEARCH_EXTENSIONS = [
|
|
3
|
+
".usd",
|
|
4
|
+
".usda",
|
|
5
|
+
".usdc",
|
|
6
|
+
".usdz",
|
|
7
|
+
".png",
|
|
8
|
+
".jpg",
|
|
9
|
+
".jpeg",
|
|
10
|
+
".webp",
|
|
11
|
+
".ktx2",
|
|
12
|
+
".exr",
|
|
13
|
+
".hdr",
|
|
14
|
+
".mdl"
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_ASSET_SEARCH_ROOTS = [
|
|
17
|
+
"resource",
|
|
18
|
+
"resources",
|
|
19
|
+
"asset",
|
|
20
|
+
"assets",
|
|
21
|
+
"img",
|
|
22
|
+
"image",
|
|
23
|
+
"images",
|
|
24
|
+
"texture",
|
|
25
|
+
"textures",
|
|
26
|
+
"material",
|
|
27
|
+
"materials"
|
|
28
|
+
];
|
|
29
|
+
const DEFAULT_MAX_ASSET_REFERENCES = 80;
|
|
30
|
+
const DEFAULT_MAX_ASSET_REFERENCE_DEPTH = 4;
|
|
31
|
+
const DEFAULT_PACKAGE_REMAP_ALIAS_COUNT = 16;
|
|
32
|
+
const USD_LAYER_EXTENSIONS = new Set([
|
|
33
|
+
".usd",
|
|
34
|
+
".usda",
|
|
35
|
+
".usdc"
|
|
36
|
+
]);
|
|
37
|
+
const TEXTURE_DIRECTORY_NAMES = new Set([
|
|
38
|
+
"img",
|
|
39
|
+
"image",
|
|
40
|
+
"images",
|
|
41
|
+
"texture",
|
|
42
|
+
"textures"
|
|
43
|
+
]);
|
|
44
|
+
const ABSOLUTE_OR_SCHEME_PATH_PATTERN = /^(?:\/|[A-Za-z][A-Za-z0-9+.-]*:)/;
|
|
45
|
+
function toFetchableUrl(value) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(value, globalThis.location?.href);
|
|
48
|
+
return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "file:" ? url.href : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function normalizeFsRelativePath(path) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
for (const part of path.replaceAll("\\", "/").split("/")) {
|
|
56
|
+
if (!part || part === ".") continue;
|
|
57
|
+
if (part === "..") {
|
|
58
|
+
parts.pop();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
parts.push(part);
|
|
62
|
+
}
|
|
63
|
+
return parts.join("/");
|
|
64
|
+
}
|
|
65
|
+
function toLayerRelativeAssetPath(assetPath) {
|
|
66
|
+
if (!assetPath || assetPath.startsWith("./") || assetPath.startsWith("../") || ABSOLUTE_OR_SCHEME_PATH_PATTERN.test(assetPath)) return assetPath;
|
|
67
|
+
return `./${assetPath}`;
|
|
68
|
+
}
|
|
69
|
+
function dirname(path) {
|
|
70
|
+
const normalized = normalizeFsRelativePath(path);
|
|
71
|
+
const index = normalized.lastIndexOf("/");
|
|
72
|
+
return index === -1 ? "" : normalized.slice(0, index);
|
|
73
|
+
}
|
|
74
|
+
function joinRelativePath(base, path) {
|
|
75
|
+
return normalizeFsRelativePath(base ? `${base}/${path}` : path);
|
|
76
|
+
}
|
|
77
|
+
function joinFsPath(base, path) {
|
|
78
|
+
return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
79
|
+
}
|
|
80
|
+
function fsDirname(path) {
|
|
81
|
+
const normalized = path.replace(/\/+$/, "");
|
|
82
|
+
const index = normalized.lastIndexOf("/");
|
|
83
|
+
if (index <= 0) return "/";
|
|
84
|
+
return normalized.slice(0, index);
|
|
85
|
+
}
|
|
86
|
+
function extensionOf(path) {
|
|
87
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(path.split(/[?#]/)[0] ?? path);
|
|
88
|
+
return match ? `.${match[1]?.toLowerCase()}` : "";
|
|
89
|
+
}
|
|
90
|
+
function contentTypeForPath(path) {
|
|
91
|
+
switch (extensionOf(path)) {
|
|
92
|
+
case ".png": return "image/png";
|
|
93
|
+
case ".jpg":
|
|
94
|
+
case ".jpeg": return "image/jpeg";
|
|
95
|
+
case ".webp": return "image/webp";
|
|
96
|
+
default: return "application/octet-stream";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function baseName(path) {
|
|
100
|
+
return normalizeFsRelativePath(path).split("/").pop() ?? "";
|
|
101
|
+
}
|
|
102
|
+
function fileNameForSource(sourcePath) {
|
|
103
|
+
const name = (sourcePath.split(/[?#]/)[0] ?? sourcePath).split("/").filter(Boolean).pop();
|
|
104
|
+
return name && /\.[a-z0-9]+$/i.test(name) ? name : "scene.usda";
|
|
105
|
+
}
|
|
106
|
+
function toArrayBuffer(data) {
|
|
107
|
+
const buffer = new ArrayBuffer(data.byteLength);
|
|
108
|
+
new Uint8Array(buffer).set(data);
|
|
109
|
+
return buffer;
|
|
110
|
+
}
|
|
111
|
+
function trimAssetReference(value) {
|
|
112
|
+
let ref = value.trim();
|
|
113
|
+
if (!ref || ref.length > 220) return null;
|
|
114
|
+
ref = ref.replace(/^[@<"']+/, "").replace(/[@>"')\]},;]+$/g, "");
|
|
115
|
+
if (!ref || ref.startsWith("#") || ref.startsWith("$")) return null;
|
|
116
|
+
if (/^[A-Za-z]:[\\/]/.test(ref)) return null;
|
|
117
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(ref)) return null;
|
|
118
|
+
if (ref.startsWith("/")) return null;
|
|
119
|
+
if (/[\0\r\n\t\f\v]/.test(ref)) return null;
|
|
120
|
+
if (!/^[ A-Za-z0-9._~!$&'()+,;=@/-]+$/.test(ref)) return null;
|
|
121
|
+
return ref;
|
|
122
|
+
}
|
|
123
|
+
function buildExtensionVariants(path, searchExtensions) {
|
|
124
|
+
const normalized = normalizeFsRelativePath(path);
|
|
125
|
+
if (!normalized) return [];
|
|
126
|
+
if (extensionOf(normalized)) return [normalized];
|
|
127
|
+
return [...searchExtensions.map((extension) => `${normalized}${extension}`), normalized];
|
|
128
|
+
}
|
|
129
|
+
function buildPackageRemapAliases(sourceRelativePath, assetPath) {
|
|
130
|
+
const extension = extensionOf(assetPath);
|
|
131
|
+
if (!extension || USD_LAYER_EXTENSIONS.has(extension)) return [];
|
|
132
|
+
const name = baseName(assetPath);
|
|
133
|
+
if (!name) return [];
|
|
134
|
+
const aliasDirs = new Set([dirname(sourceRelativePath)]);
|
|
135
|
+
const assetDir = dirname(assetPath);
|
|
136
|
+
const assetParentDir = dirname(assetDir);
|
|
137
|
+
const assetTextureDir = baseName(assetDir).toLowerCase();
|
|
138
|
+
if (TEXTURE_DIRECTORY_NAMES.has(assetTextureDir)) aliasDirs.add(assetParentDir);
|
|
139
|
+
return unique([...aliasDirs].flatMap((aliasDir) => Array.from({ length: DEFAULT_PACKAGE_REMAP_ALIAS_COUNT }, (_, index) => joinRelativePath(aliasDir, `${index}/${name}`))));
|
|
140
|
+
}
|
|
141
|
+
function addPackageRemapAliases(files, aliases, data, ownedAliases, aliasConflicts, existingFiles = {}) {
|
|
142
|
+
for (const alias of aliases) {
|
|
143
|
+
if (aliasConflicts.has(alias)) continue;
|
|
144
|
+
const existingInput = existingFiles[alias];
|
|
145
|
+
if (existingInput && existingInput !== data) {
|
|
146
|
+
aliasConflicts.add(alias);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const existing = files[alias];
|
|
150
|
+
if (existing && existing !== data) {
|
|
151
|
+
if (ownedAliases.has(alias)) {
|
|
152
|
+
delete files[alias];
|
|
153
|
+
ownedAliases.delete(alias);
|
|
154
|
+
}
|
|
155
|
+
aliasConflicts.add(alias);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!existing) {
|
|
159
|
+
files[alias] = data;
|
|
160
|
+
ownedAliases.add(alias);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function unique(values) {
|
|
165
|
+
return [...new Set(values)];
|
|
166
|
+
}
|
|
167
|
+
function uniqueFileCount(files) {
|
|
168
|
+
return new Set(Object.values(files)).size;
|
|
169
|
+
}
|
|
170
|
+
function firstPathSegment(path) {
|
|
171
|
+
return normalizeFsRelativePath(path).split("/")[0] ?? "";
|
|
172
|
+
}
|
|
173
|
+
function normalizeSearchRoots(roots) {
|
|
174
|
+
return unique((roots ?? DEFAULT_ASSET_SEARCH_ROOTS).map((root) => normalizeFsRelativePath(root)).filter(Boolean));
|
|
175
|
+
}
|
|
176
|
+
function inferReferencedSearchRoots(refs, searchRoots) {
|
|
177
|
+
const rootSet = new Set(searchRoots);
|
|
178
|
+
return unique(refs.map((ref) => firstPathSegment(ref)).filter((segment) => rootSet.has(segment)));
|
|
179
|
+
}
|
|
180
|
+
function buildFetchAttempts(ref, sourceUrl, sourceRelativePath, searchExtensions, rootSourceUrl, searchRoots) {
|
|
181
|
+
const sourceDir = dirname(sourceRelativePath);
|
|
182
|
+
const attempts = [];
|
|
183
|
+
const seen = /* @__PURE__ */ new Set();
|
|
184
|
+
const normalizedRef = normalizeFsRelativePath(ref);
|
|
185
|
+
const isDotRelative = /^\.{1,2}\//.test(ref);
|
|
186
|
+
const candidates = [];
|
|
187
|
+
function addCandidate(fsPath, fetchPath, baseUrl, aliases = []) {
|
|
188
|
+
const normalizedFsPath = normalizeFsRelativePath(fsPath);
|
|
189
|
+
if (!normalizedFsPath) return;
|
|
190
|
+
candidates.push({
|
|
191
|
+
fsPath: normalizedFsPath,
|
|
192
|
+
fetchPath,
|
|
193
|
+
baseUrl,
|
|
194
|
+
aliases: unique([
|
|
195
|
+
normalizedRef,
|
|
196
|
+
normalizedFsPath,
|
|
197
|
+
...aliases
|
|
198
|
+
].filter(Boolean))
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (!isDotRelative) {
|
|
202
|
+
for (const root of searchRoots) if (normalizedRef !== root && !normalizedRef.startsWith(`${root}/`)) addCandidate(`${root}/${normalizedRef}`, `${root}/${normalizedRef}`, rootSourceUrl);
|
|
203
|
+
}
|
|
204
|
+
addCandidate(joinRelativePath(sourceDir, ref), ref, sourceUrl);
|
|
205
|
+
if (!isDotRelative && sourceDir) addCandidate(normalizedRef, normalizedRef, rootSourceUrl);
|
|
206
|
+
for (const candidate of candidates) {
|
|
207
|
+
const fsAliases = new Set(candidate.aliases);
|
|
208
|
+
for (const pathWithExtension of buildExtensionVariants(candidate.fsPath, searchExtensions)) {
|
|
209
|
+
const extension = extensionOf(pathWithExtension);
|
|
210
|
+
fsAliases.add(pathWithExtension);
|
|
211
|
+
const fetchPath = extensionOf(candidate.fetchPath) ? candidate.fetchPath : `${candidate.fetchPath}${extension}`;
|
|
212
|
+
try {
|
|
213
|
+
const url = new URL(fetchPath, candidate.baseUrl).href;
|
|
214
|
+
if (seen.has(url)) continue;
|
|
215
|
+
seen.add(url);
|
|
216
|
+
attempts.push({
|
|
217
|
+
url,
|
|
218
|
+
fsPaths: [...fsAliases],
|
|
219
|
+
sourceRelativePath: pathWithExtension,
|
|
220
|
+
packageRemapAliases: buildPackageRemapAliases(sourceRelativePath, pathWithExtension)
|
|
221
|
+
});
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return attempts;
|
|
226
|
+
}
|
|
227
|
+
async function fetchBinary(url) {
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch(url);
|
|
230
|
+
if (!response.ok) return null;
|
|
231
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function extractedAssetReferences(extracted) {
|
|
237
|
+
return unique([
|
|
238
|
+
...extracted.subLayers,
|
|
239
|
+
...extracted.references,
|
|
240
|
+
...extracted.payloads
|
|
241
|
+
].map((ref) => trimAssetReference(ref)).filter((ref) => !!ref));
|
|
242
|
+
}
|
|
243
|
+
function isQueueableUsdLayer(path) {
|
|
244
|
+
return USD_LAYER_EXTENSIONS.has(extensionOf(path));
|
|
245
|
+
}
|
|
246
|
+
function shouldFetchAssetReference(ref, searchExtensions) {
|
|
247
|
+
const extension = extensionOf(ref);
|
|
248
|
+
return !extension || searchExtensions.includes(extension);
|
|
249
|
+
}
|
|
250
|
+
function existingFsPath(pxr, directory, relativePaths, files) {
|
|
251
|
+
for (const relativePath of relativePaths) if (files[relativePath] || pxr.FS.exists(joinFsPath(directory, relativePath))) return relativePath;
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function urlForSourceRelativePath(sourceRelativePath, rootSourceUrl) {
|
|
255
|
+
try {
|
|
256
|
+
return new URL(sourceRelativePath, rootSourceUrl).href;
|
|
257
|
+
} catch {
|
|
258
|
+
return rootSourceUrl;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function queueUsdLayer(queue, queuedLayers, pxr, directory, sourceRelativePath, sourceUrl, rootSourceUrl, searchRoots, depth) {
|
|
262
|
+
if (!isQueueableUsdLayer(sourceRelativePath)) return;
|
|
263
|
+
const filePath = joinFsPath(directory, sourceRelativePath);
|
|
264
|
+
if (!pxr.FS.exists(filePath)) return;
|
|
265
|
+
const queueKey = sourceRelativePath;
|
|
266
|
+
if (queuedLayers.has(queueKey)) return;
|
|
267
|
+
queuedLayers.add(queueKey);
|
|
268
|
+
queue.push({
|
|
269
|
+
filePath,
|
|
270
|
+
sourceUrl,
|
|
271
|
+
rootSourceUrl,
|
|
272
|
+
sourceRelativePath,
|
|
273
|
+
searchRoots,
|
|
274
|
+
depth
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function autoResolveAssetFiles(pxr, rootFilePath, options) {
|
|
278
|
+
const sourcePath = options.sourcePath ?? "";
|
|
279
|
+
if (options.autoResolveAssets === false || !sourcePath) return {};
|
|
280
|
+
const sourceUrl = toFetchableUrl(sourcePath);
|
|
281
|
+
if (!sourceUrl) return {};
|
|
282
|
+
const searchExtensions = options.assetSearchExtensions ?? DEFAULT_ASSET_SEARCH_EXTENSIONS;
|
|
283
|
+
const configuredSearchRoots = normalizeSearchRoots(options.assetSearchRoots);
|
|
284
|
+
const maxFiles = options.maxAssetReferences ?? DEFAULT_MAX_ASSET_REFERENCES;
|
|
285
|
+
const maxDepth = options.maxAssetReferenceDepth ?? DEFAULT_MAX_ASSET_REFERENCE_DEPTH;
|
|
286
|
+
const rootRelativePath = normalizeFsRelativePath(options.fileName ?? fileNameForSource(sourcePath));
|
|
287
|
+
const directory = fsDirname(rootFilePath);
|
|
288
|
+
const files = {};
|
|
289
|
+
const ownedPackageAliases = /* @__PURE__ */ new Set();
|
|
290
|
+
const packageAliasConflicts = /* @__PURE__ */ new Set();
|
|
291
|
+
const fetchedUrls = /* @__PURE__ */ new Set();
|
|
292
|
+
const queuedLayers = /* @__PURE__ */ new Set();
|
|
293
|
+
const queue = [{
|
|
294
|
+
filePath: rootFilePath,
|
|
295
|
+
sourceUrl,
|
|
296
|
+
rootSourceUrl: sourceUrl,
|
|
297
|
+
sourceRelativePath: rootRelativePath,
|
|
298
|
+
searchRoots: inferReferencedSearchRoots([rootRelativePath], configuredSearchRoots),
|
|
299
|
+
depth: 0
|
|
300
|
+
}];
|
|
301
|
+
queuedLayers.add(rootRelativePath);
|
|
302
|
+
while (queue.length > 0 && uniqueFileCount(files) < maxFiles) {
|
|
303
|
+
const item = queue.shift();
|
|
304
|
+
if (!item || item.depth > maxDepth) continue;
|
|
305
|
+
const refs = extractedAssetReferences(pxr.UsdUtils.ExtractExternalReferences(item.filePath)).filter((ref) => shouldFetchAssetReference(ref, searchExtensions));
|
|
306
|
+
const itemSearchRoots = unique([
|
|
307
|
+
...item.searchRoots,
|
|
308
|
+
...inferReferencedSearchRoots(refs, configuredSearchRoots),
|
|
309
|
+
...inferReferencedSearchRoots([item.sourceRelativePath], configuredSearchRoots)
|
|
310
|
+
]);
|
|
311
|
+
for (const ref of refs) {
|
|
312
|
+
if (uniqueFileCount(files) >= maxFiles) break;
|
|
313
|
+
for (const attempt of buildFetchAttempts(ref, item.sourceUrl, item.sourceRelativePath, searchExtensions, item.rootSourceUrl, itemSearchRoots)) {
|
|
314
|
+
const existingPath = existingFsPath(pxr, directory, [attempt.sourceRelativePath], files);
|
|
315
|
+
if (existingPath) {
|
|
316
|
+
if (item.depth < maxDepth) queueUsdLayer(queue, queuedLayers, pxr, directory, existingPath, urlForSourceRelativePath(existingPath, item.rootSourceUrl), item.rootSourceUrl, itemSearchRoots, item.depth + 1);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
if (fetchedUrls.has(attempt.url)) continue;
|
|
320
|
+
fetchedUrls.add(attempt.url);
|
|
321
|
+
const data = await fetchBinary(attempt.url);
|
|
322
|
+
if (!data) continue;
|
|
323
|
+
for (const fsPath of attempt.fsPaths) {
|
|
324
|
+
ownedPackageAliases.delete(fsPath);
|
|
325
|
+
files[fsPath] = data;
|
|
326
|
+
if (isQueueableUsdLayer(fsPath)) pxr.FS.writeFile(joinFsPath(directory, fsPath), data);
|
|
327
|
+
}
|
|
328
|
+
addPackageRemapAliases(files, attempt.packageRemapAliases, data, ownedPackageAliases, packageAliasConflicts);
|
|
329
|
+
if (item.depth < maxDepth) queueUsdLayer(queue, queuedLayers, pxr, directory, attempt.sourceRelativePath, attempt.url, item.rootSourceUrl, itemSearchRoots, item.depth + 1);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return files;
|
|
335
|
+
}
|
|
336
|
+
function isMetadataAssetReference(value, searchExtensions) {
|
|
337
|
+
if (/^(blob|data):/i.test(value)) return false;
|
|
338
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return false;
|
|
339
|
+
if (value.startsWith("/")) return false;
|
|
340
|
+
return searchExtensions.includes(extensionOf(value));
|
|
341
|
+
}
|
|
342
|
+
function collectMetadataAssetReferences(value, searchExtensions, refs = /* @__PURE__ */ new Set()) {
|
|
343
|
+
if (!value) return refs;
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
for (const item of value) collectMetadataAssetReferences(item, searchExtensions, refs);
|
|
346
|
+
return refs;
|
|
347
|
+
}
|
|
348
|
+
if (typeof value !== "object") return refs;
|
|
349
|
+
const asset = value;
|
|
350
|
+
for (const item of [asset.path, asset.url]) if (typeof item === "string" && isMetadataAssetReference(item, searchExtensions)) refs.add(item);
|
|
351
|
+
for (const item of Object.values(value)) collectMetadataAssetReferences(item, searchExtensions, refs);
|
|
352
|
+
return refs;
|
|
353
|
+
}
|
|
354
|
+
async function autoResolveTextureFiles(metadata, existingFiles, options) {
|
|
355
|
+
const sourcePath = options.sourcePath ?? "";
|
|
356
|
+
if (options.autoResolveAssets === false || !sourcePath) return {};
|
|
357
|
+
const sourceUrl = toFetchableUrl(sourcePath);
|
|
358
|
+
if (!sourceUrl) return {};
|
|
359
|
+
const searchExtensions = options.assetSearchExtensions ?? DEFAULT_ASSET_SEARCH_EXTENSIONS;
|
|
360
|
+
const configuredSearchRoots = normalizeSearchRoots(options.assetSearchRoots);
|
|
361
|
+
const textureRefs = [...collectMetadataAssetReferences(metadata, searchExtensions)];
|
|
362
|
+
const searchRoots = unique([...inferReferencedSearchRoots(Object.keys(existingFiles), configuredSearchRoots), ...inferReferencedSearchRoots(textureRefs, configuredSearchRoots)]);
|
|
363
|
+
const maxFiles = options.maxAssetReferences ?? DEFAULT_MAX_ASSET_REFERENCES;
|
|
364
|
+
const remainingFiles = Math.max(maxFiles - uniqueFileCount(existingFiles), 0);
|
|
365
|
+
if (remainingFiles === 0) return {};
|
|
366
|
+
const rootRelativePath = normalizeFsRelativePath(options.fileName ?? fileNameForSource(sourcePath));
|
|
367
|
+
const files = {};
|
|
368
|
+
const ownedPackageAliases = /* @__PURE__ */ new Set();
|
|
369
|
+
const packageAliasConflicts = /* @__PURE__ */ new Set();
|
|
370
|
+
const fetchedUrls = /* @__PURE__ */ new Set();
|
|
371
|
+
let resolvedFiles = 0;
|
|
372
|
+
for (const ref of textureRefs) {
|
|
373
|
+
if (resolvedFiles >= remainingFiles) break;
|
|
374
|
+
for (const attempt of buildFetchAttempts(ref, sourceUrl, rootRelativePath, searchExtensions, sourceUrl, searchRoots)) {
|
|
375
|
+
if (attempt.fsPaths.some((path) => existingFiles[path] || files[path])) break;
|
|
376
|
+
if (fetchedUrls.has(attempt.url)) continue;
|
|
377
|
+
fetchedUrls.add(attempt.url);
|
|
378
|
+
const data = await fetchBinary(attempt.url);
|
|
379
|
+
if (!data) continue;
|
|
380
|
+
for (const fsPath of attempt.fsPaths) {
|
|
381
|
+
ownedPackageAliases.delete(fsPath);
|
|
382
|
+
files[fsPath] = data;
|
|
383
|
+
}
|
|
384
|
+
addPackageRemapAliases(files, attempt.packageRemapAliases, data, ownedPackageAliases, packageAliasConflicts, existingFiles);
|
|
385
|
+
resolvedFiles += 1;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return files;
|
|
390
|
+
}
|
|
391
|
+
function readZipEntryName(data, start, length) {
|
|
392
|
+
return new TextDecoder().decode(data.subarray(start, start + length));
|
|
393
|
+
}
|
|
394
|
+
function extractStoredZipEntries(data) {
|
|
395
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
396
|
+
const entries = /* @__PURE__ */ new Map();
|
|
397
|
+
let offset = 0;
|
|
398
|
+
while (offset + 30 <= data.byteLength) {
|
|
399
|
+
if (view.getUint32(offset, true) !== 67324752) break;
|
|
400
|
+
const method = view.getUint16(offset + 8, true);
|
|
401
|
+
const compressedSize = view.getUint32(offset + 18, true);
|
|
402
|
+
const fileNameLength = view.getUint16(offset + 26, true);
|
|
403
|
+
const extraLength = view.getUint16(offset + 28, true);
|
|
404
|
+
const nameStart = offset + 30;
|
|
405
|
+
const dataStart = nameStart + fileNameLength + extraLength;
|
|
406
|
+
const dataEnd = dataStart + compressedSize;
|
|
407
|
+
if (dataEnd > data.byteLength) break;
|
|
408
|
+
const name = normalizeFsRelativePath(readZipEntryName(data, nameStart, fileNameLength));
|
|
409
|
+
if (name && method === 0) entries.set(name, data.slice(dataStart, dataEnd));
|
|
410
|
+
offset = dataEnd;
|
|
411
|
+
}
|
|
412
|
+
return entries;
|
|
413
|
+
}
|
|
414
|
+
function createTextureResolverFromEntries(entries) {
|
|
415
|
+
if (typeof Blob === "undefined" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return null;
|
|
416
|
+
const imageEntries = new Map([...entries].filter(([path]) => [
|
|
417
|
+
".png",
|
|
418
|
+
".jpg",
|
|
419
|
+
".jpeg",
|
|
420
|
+
".webp"
|
|
421
|
+
].includes(extensionOf(path))));
|
|
422
|
+
if (imageEntries.size === 0) return null;
|
|
423
|
+
const byBaseName = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const path of imageEntries.keys()) {
|
|
425
|
+
const name = baseName(path);
|
|
426
|
+
byBaseName.set(name, byBaseName.has(name) ? null : path);
|
|
427
|
+
}
|
|
428
|
+
const urls = /* @__PURE__ */ new Map();
|
|
429
|
+
const urlForEntry = (path) => {
|
|
430
|
+
const entry = imageEntries.get(path);
|
|
431
|
+
if (!entry) return null;
|
|
432
|
+
let url = urls.get(path);
|
|
433
|
+
if (!url) {
|
|
434
|
+
url = URL.createObjectURL(new Blob([toArrayBuffer(entry)], { type: contentTypeForPath(path) }));
|
|
435
|
+
urls.set(path, url);
|
|
436
|
+
}
|
|
437
|
+
return url;
|
|
438
|
+
};
|
|
439
|
+
const resolvePath = (value) => {
|
|
440
|
+
if (!value) return null;
|
|
441
|
+
const normalized = normalizeFsRelativePath(value.split(/[?#]/)[0] ?? value);
|
|
442
|
+
if (!normalized) return null;
|
|
443
|
+
if (imageEntries.has(normalized)) return urlForEntry(normalized);
|
|
444
|
+
const suffixMatches = [...imageEntries.keys()].filter((path) => path.endsWith(`/${normalized}`));
|
|
445
|
+
if (suffixMatches.length === 1 && suffixMatches[0]) return urlForEntry(suffixMatches[0]);
|
|
446
|
+
const virtualFsMatches = [...imageEntries.keys()].filter((path) => normalized.endsWith(`/${path}`));
|
|
447
|
+
if (virtualFsMatches.length === 1 && virtualFsMatches[0]) return urlForEntry(virtualFsMatches[0]);
|
|
448
|
+
const uniqueBaseName = byBaseName.get(baseName(normalized));
|
|
449
|
+
return uniqueBaseName ? urlForEntry(uniqueBaseName) : null;
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
urls,
|
|
453
|
+
resolve(asset) {
|
|
454
|
+
return resolvePath(asset.resolvedPath) ?? resolvePath(asset.path) ?? resolvePath(asset.url) ?? null;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function createPackageTextureResolver(data) {
|
|
459
|
+
return createTextureResolverFromEntries(extractStoredZipEntries(data));
|
|
460
|
+
}
|
|
461
|
+
function findPackageRootLayer(data) {
|
|
462
|
+
for (const path of extractStoredZipEntries(data).keys()) if (USD_LAYER_EXTENSIONS.has(extensionOf(path))) return path;
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
export { autoResolveAssetFiles, autoResolveTextureFiles, createPackageTextureResolver, createTextureResolverFromEntries, findPackageRootLayer, toLayerRelativeAssetPath };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openusd-wasm/utils",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"description": "Shared OpenUSD WebAssembly utilities for asset resolution and USDZ packaging workflows.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/openusd-wasm/openusd-pxr-wasm",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/openusd-wasm/openusd-pxr-wasm.git",
|
|
11
|
+
"directory": "packages/utils"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/openusd_utils.js",
|
|
14
|
+
"module": "./dist/openusd_utils.js",
|
|
15
|
+
"types": "./dist/openusd_utils.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/openusd_utils.d.ts",
|
|
19
|
+
"import": "./dist/openusd_utils.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@openusd-wasm/pxr": "0.0.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.6.2",
|
|
31
|
+
"@typescript/native-preview": "7.0.0-dev.20260509.2",
|
|
32
|
+
"tsdown": "^0.22.0",
|
|
33
|
+
"typescript": "^6.0.3",
|
|
34
|
+
"vitest": "^4.1.5"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"dev": "tsdown --watch",
|
|
42
|
+
"debug": "tsdown --sourcemap",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|