查看器认得 ASCII FBX 的字节开头

- packages/model3d-viewer/src/loader.ts:字节魔数多认一种 ASCII FBX 开头(`; FBX`,Blender 等工具的默认导出),此前这批文件在没显式给 format 时会被判 unsupported-format,尽管 FBXLoader 能解析
- packages/model3d-viewer/src/loader.ts:BOM / 前导空白的跳过抽成 resolveModel3dViewerContentStart,JSON 与 ASCII FBX 两处判据共用
- packages/model3d-viewer/src/loader.test.ts:补 ASCII FBX(含前导空白)的字节判定用例
This commit is contained in:
2026-09-24 17:24:28 +08:00
parent 0c0819249d
commit 61cfb8800d
2 changed files with 41 additions and 5 deletions
@@ -84,6 +84,19 @@ describe('模型格式判定', () => {
new TextEncoder().encode('\uFEFF{"asset":{}}'),
),
).toBe('gltf');
// ASCII FBX(Blender 默认导出)没有 Kaydara 魔数,按开头这行认。
expect(
resolveModel3dViewerFormatFromBytes(
new TextEncoder().encode(
'; FBX 7.4.0 project file\nFBXHeaderExtension: {',
),
),
).toBe('fbx');
expect(
resolveModel3dViewerFormatFromBytes(
new TextEncoder().encode('\n ; FBX 7.3.0 project file'),
),
).toBe('fbx');
expect(
resolveModel3dViewerFormatFromBytes(new Uint8Array([0x00, 0x01])),
).toBeNull();
+28 -5
View File
@@ -43,6 +43,8 @@ const MODEL3D_VIEWER_FBX_MAGIC = [
0x4b, 0x61, 0x79, 0x64, 0x61, 0x72, 0x61, 0x20, 0x46, 0x42, 0x58, 0x20, 0x42,
0x69, 0x6e, 0x61, 0x72, 0x79,
];
/** ASCII FBX 的开头:「; FBX 7.4.0 project file」(Blender 等工具的默认导出)。 */
const MODEL3D_VIEWER_ASCII_FBX_MAGIC = [0x3b, 0x20, 0x46, 0x42, 0x58];
const MODEL3D_VIEWER_SUPPORTED_FORMAT_TEXT =
MODEL3D_VIEWER_SUPPORTED_FORMATS.join(' / ');
@@ -56,18 +58,36 @@ function matchesModel3dViewerMagic(
return magic.every((value, index) => bytes[index] === value);
}
function isModel3dViewerJsonStart(bytes: Uint8Array) {
for (const byte of bytes) {
// BOM 与空白之后是 `{` 才当 glTF JSON:多文件 glTF 的正文就是一个 JSON 文档。
/** 跳过 BOM 与前导空白后的正文起点;全是噪声时返回 -1。 */
function resolveModel3dViewerContentStart(bytes: Uint8Array) {
for (let index = 0; index < bytes.length; index += 1) {
const byte = bytes[index];
if (byte === 0xef || byte === 0xbb || byte === 0xbf) {
continue;
}
if (byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d) {
continue;
}
return byte === 0x7b;
return index;
}
return false;
return -1;
}
function isModel3dViewerJsonStart(bytes: Uint8Array) {
// BOM 与空白之后是 `{` 才当 glTF JSON:多文件 glTF 的正文就是一个 JSON 文档。
const start = resolveModel3dViewerContentStart(bytes);
return start >= 0 && bytes[start] === 0x7b;
}
/** ASCII FBX 只有「看起来像」的判据,识别不出就交给显式 format 或扩展名。 */
function isModel3dViewerAsciiFbxStart(bytes: Uint8Array) {
const start = resolveModel3dViewerContentStart(bytes);
if (start < 0) {
return false;
}
return MODEL3D_VIEWER_ASCII_FBX_MAGIC.every(
(value, offset) => bytes[start + offset] === value,
);
}
/** 声明类型里的模型格式;不是可渲染格式时返回 null。 */
@@ -96,6 +116,9 @@ export function resolveModel3dViewerFormatFromBytes(
if (matchesModel3dViewerMagic(view, MODEL3D_VIEWER_FBX_MAGIC)) {
return 'fbx';
}
if (isModel3dViewerAsciiFbxStart(view)) {
return 'fbx';
}
if (isModel3dViewerJsonStart(view)) {
return 'gltf';
}