@galfus/gltf-loader 0.0.0-stage → 0.31.0-alpha
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/README.md +93 -2
- package/package.json +43 -4
- package/src/animation.ts +160 -0
- package/src/bounds.ts +160 -0
- package/src/decoders/resolver.ts +131 -0
- package/src/factory.ts +303 -0
- package/src/index.ts +185 -0
- package/src/instance.ts +126 -0
- package/src/parser.ts +334 -0
- package/src/resources.ts +391 -0
- package/src/scene.ts +354 -0
- package/src/types.ts +270 -0
- package/src/zip.ts +173 -0
package/src/zip.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory ZIP archive unpacking and URI resolver for multi-file glTF assets.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { unzipSync } from 'fflate';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks if the binary buffer starts with the standard PKZIP magic bytes (PK\x03\x04).
|
|
9
|
+
*
|
|
10
|
+
* @param buffer Raw binary buffer to check.
|
|
11
|
+
* @returns True if the buffer is a valid PKZIP container.
|
|
12
|
+
*/
|
|
13
|
+
export function isZipContainer(buffer: Uint8Array): boolean {
|
|
14
|
+
if (buffer.byteLength < 4) return false;
|
|
15
|
+
return (
|
|
16
|
+
buffer[0] === 0x50 && // 'P'
|
|
17
|
+
buffer[1] === 0x4b && // 'K'
|
|
18
|
+
((buffer[2] === 0x03 && buffer[3] === 0x04) || // Standard local file header
|
|
19
|
+
(buffer[2] === 0x05 && buffer[3] === 0x06) || // Empty archive end of central dir
|
|
20
|
+
(buffer[2] === 0x07 && buffer[3] === 0x08)) // Spanned archive marker
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Unpacks a ZIP archive into a normalized in-memory file dictionary.
|
|
26
|
+
*
|
|
27
|
+
* Directory entries are discarded; forward slashes are standardized.
|
|
28
|
+
*
|
|
29
|
+
* @param buffer The raw ZIP bytes.
|
|
30
|
+
* @returns Map of normalized file paths to their decompressed bytes.
|
|
31
|
+
*/
|
|
32
|
+
export function unpackZipArchive(buffer: Uint8Array): Map<string, Uint8Array> {
|
|
33
|
+
const unzipped = unzipSync(buffer);
|
|
34
|
+
const fileMap = new Map<string, Uint8Array>();
|
|
35
|
+
|
|
36
|
+
for (const rawPath of Object.keys(unzipped)) {
|
|
37
|
+
const data = unzipped[rawPath];
|
|
38
|
+
if (!data || data.byteLength === 0) {
|
|
39
|
+
// Could be an empty directory entry or empty file
|
|
40
|
+
if (rawPath.endsWith('/') || rawPath.endsWith('\\')) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Normalize Windows backslashes to forward slashes and strip leading slashes
|
|
46
|
+
const normalized = rawPath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
47
|
+
if (normalized.length > 0 && !normalized.endsWith('/')) {
|
|
48
|
+
fileMap.set(normalized, data!);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return fileMap;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Locates the primary .gltf or .glb file inside an unpacked archive.
|
|
57
|
+
*
|
|
58
|
+
* Prioritizes standard names like `scene.gltf` or `model.gltf`, or the shallowest glTF file.
|
|
59
|
+
*
|
|
60
|
+
* @param fileMap The unpacked files map.
|
|
61
|
+
* @returns The path of the primary glTF file, or null if none found.
|
|
62
|
+
*/
|
|
63
|
+
export function findPrimaryGltfPath(fileMap: Map<string, Uint8Array>): string | null {
|
|
64
|
+
const gltfCandidates: string[] = [];
|
|
65
|
+
|
|
66
|
+
for (const path of fileMap.keys()) {
|
|
67
|
+
const lower = path.toLowerCase();
|
|
68
|
+
if (lower.endsWith('.gltf') || lower.endsWith('.glb')) {
|
|
69
|
+
gltfCandidates.push(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (gltfCandidates.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for canonical model names first
|
|
78
|
+
const preferred = gltfCandidates.find((p) => {
|
|
79
|
+
const base = p.split('/').pop()?.toLowerCase();
|
|
80
|
+
return (
|
|
81
|
+
base === 'scene.gltf' || base === 'model.gltf' || base === 'scene.glb' || base === 'model.glb'
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
if (preferred) return preferred;
|
|
85
|
+
|
|
86
|
+
// Otherwise sort by directory depth (shallowest first) and then by name
|
|
87
|
+
gltfCandidates.sort((a, b) => {
|
|
88
|
+
const depthA = a.split('/').length;
|
|
89
|
+
const depthB = b.split('/').length;
|
|
90
|
+
if (depthA !== depthB) return depthA - depthB;
|
|
91
|
+
return a.localeCompare(b);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return gltfCandidates[0]!;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves a relative path given a base directory.
|
|
99
|
+
*/
|
|
100
|
+
function resolveRelativePath(baseDir: string, relativePath: string): string {
|
|
101
|
+
// Strip leading ./
|
|
102
|
+
let cleanRel = relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
103
|
+
|
|
104
|
+
if (!baseDir || baseDir === '.') {
|
|
105
|
+
return cleanRel;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parts = (baseDir + '/' + cleanRel).split('/');
|
|
109
|
+
const resolvedParts: string[] = [];
|
|
110
|
+
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
if (!part || part === '.') continue;
|
|
113
|
+
if (part === '..') {
|
|
114
|
+
resolvedParts.pop();
|
|
115
|
+
} else {
|
|
116
|
+
resolvedParts.push(part);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return resolvedParts.join('/');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates an external resolver callback backed by an in-memory ZIP archive file map.
|
|
125
|
+
*
|
|
126
|
+
* @param fileMap The unpacked files map.
|
|
127
|
+
* @param gltfPath The path to the primary glTF file used as reference for relative paths.
|
|
128
|
+
* @returns An async resolver function matching the glTF loader resolver interface.
|
|
129
|
+
*/
|
|
130
|
+
export function createZipResolver(
|
|
131
|
+
fileMap: Map<string, Uint8Array>,
|
|
132
|
+
gltfPath: string
|
|
133
|
+
): (uri: string) => Promise<Uint8Array> {
|
|
134
|
+
const lastSlash = gltfPath.lastIndexOf('/');
|
|
135
|
+
const baseDir = lastSlash >= 0 ? gltfPath.substring(0, lastSlash) : '';
|
|
136
|
+
|
|
137
|
+
return async (uri: string): Promise<Uint8Array> => {
|
|
138
|
+
// 1. Decode URL encoded characters (e.g. %20 -> space)
|
|
139
|
+
const decodedUri = decodeURIComponent(uri.trim());
|
|
140
|
+
|
|
141
|
+
// 2. Resolve relative to baseDir
|
|
142
|
+
const resolvedPath = resolveRelativePath(baseDir, decodedUri);
|
|
143
|
+
|
|
144
|
+
// 3. Exact match lookup
|
|
145
|
+
let match = fileMap.get(resolvedPath);
|
|
146
|
+
if (match) return match;
|
|
147
|
+
|
|
148
|
+
// 4. Fallback: try raw decodedUri directly without baseDir
|
|
149
|
+
match = fileMap.get(decodedUri);
|
|
150
|
+
if (match) return match;
|
|
151
|
+
|
|
152
|
+
// 5. Fallback: case-insensitive search or filename-only match
|
|
153
|
+
const lowerResolved = resolvedPath.toLowerCase();
|
|
154
|
+
const fileName = decodedUri.split('/').pop()?.toLowerCase();
|
|
155
|
+
|
|
156
|
+
for (const [key, data] of fileMap.entries()) {
|
|
157
|
+
const keyLower = key.toLowerCase();
|
|
158
|
+
if (keyLower === lowerResolved) {
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
if (fileName && keyLower.endsWith('/' + fileName)) {
|
|
162
|
+
return data;
|
|
163
|
+
}
|
|
164
|
+
if (fileName && keyLower === fileName) {
|
|
165
|
+
return data;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error(
|
|
170
|
+
`[GltfZipResolver] File not found in ZIP archive: "${uri}" (resolved: "${resolvedPath}")`
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
}
|