@dreamerbird/camera-studio 0.1.0 → 0.3.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.
Files changed (139) hide show
  1. package/LICENSE +866 -0
  2. package/assets/models/Y Bot.glb +0 -0
  3. package/dist/CameraScene.d.ts +1 -1
  4. package/dist/CameraScene.d.ts.map +1 -1
  5. package/dist/CameraScene.js +100 -9
  6. package/dist/PlacementManager.d.ts +15 -1
  7. package/dist/PlacementManager.d.ts.map +1 -1
  8. package/dist/PlacementManager.js +151 -15
  9. package/dist/agent/api.d.ts +5 -0
  10. package/dist/agent/api.d.ts.map +1 -1
  11. package/dist/agent/api.js +25 -0
  12. package/dist/agent/api.test.js +75 -1
  13. package/dist/agent/commands/assets.d.ts +5 -0
  14. package/dist/agent/commands/assets.d.ts.map +1 -1
  15. package/dist/agent/commands/assets.js +93 -21
  16. package/dist/agent/commands/assets.test.js +53 -0
  17. package/dist/agent/commands/materials.d.ts +18 -0
  18. package/dist/agent/commands/materials.d.ts.map +1 -0
  19. package/dist/agent/commands/materials.js +192 -0
  20. package/dist/agent/commands/materials.test.d.ts +2 -0
  21. package/dist/agent/commands/materials.test.d.ts.map +1 -0
  22. package/dist/agent/commands/materials.test.js +294 -0
  23. package/dist/agent/commands/modelRecovery.test.d.ts +2 -0
  24. package/dist/agent/commands/modelRecovery.test.d.ts.map +1 -0
  25. package/dist/agent/commands/modelRecovery.test.js +122 -0
  26. package/dist/agent/commands/models.d.ts +13 -0
  27. package/dist/agent/commands/models.d.ts.map +1 -1
  28. package/dist/agent/commands/models.js +98 -1
  29. package/dist/agent/commands/primitives.d.ts +27 -0
  30. package/dist/agent/commands/primitives.d.ts.map +1 -0
  31. package/dist/agent/commands/primitives.js +117 -0
  32. package/dist/agent/commands/primitives.test.d.ts +2 -0
  33. package/dist/agent/commands/primitives.test.d.ts.map +1 -0
  34. package/dist/agent/commands/primitives.test.js +135 -0
  35. package/dist/agent/expectedCommands.d.ts +1 -1
  36. package/dist/agent/expectedCommands.d.ts.map +1 -1
  37. package/dist/agent/expectedCommands.js +6 -0
  38. package/dist/agent/propAccess.d.ts +2 -0
  39. package/dist/agent/propAccess.d.ts.map +1 -1
  40. package/dist/agent/propAccess.js +21 -0
  41. package/dist/agent/registerCommands.d.ts.map +1 -1
  42. package/dist/agent/registerCommands.js +5 -1
  43. package/dist/agent/runtime/objectRuntime.d.ts +2 -0
  44. package/dist/agent/runtime/objectRuntime.d.ts.map +1 -1
  45. package/dist/agent/runtime/objectRuntime.js +15 -1
  46. package/dist/agent/runtime.d.ts +1 -0
  47. package/dist/agent/runtime.d.ts.map +1 -1
  48. package/dist/agent/runtime.js +2 -0
  49. package/dist/assetLibrary/dispose.d.ts +4 -2
  50. package/dist/assetLibrary/dispose.d.ts.map +1 -1
  51. package/dist/assetLibrary/dispose.js +7 -5
  52. package/dist/assetLibrary/exportGLB.d.ts +25 -0
  53. package/dist/assetLibrary/exportGLB.d.ts.map +1 -1
  54. package/dist/assetLibrary/exportGLB.js +43 -1
  55. package/dist/assetLibrary/exportGLB.test.js +56 -2
  56. package/dist/assetLibrary/gltfLoader.d.ts +2 -1
  57. package/dist/assetLibrary/gltfLoader.d.ts.map +1 -1
  58. package/dist/assetLibrary/gltfLoader.js +2 -2
  59. package/dist/assetLibrary/gltfResourceSet.d.ts +6 -0
  60. package/dist/assetLibrary/gltfResourceSet.d.ts.map +1 -0
  61. package/dist/assetLibrary/gltfResourceSet.js +27 -0
  62. package/dist/assetLibrary/gltfResourceSet.test.d.ts +2 -0
  63. package/dist/assetLibrary/gltfResourceSet.test.d.ts.map +1 -0
  64. package/dist/assetLibrary/gltfResourceSet.test.js +40 -0
  65. package/dist/assetLibrary/importModelFile.d.ts +5 -0
  66. package/dist/assetLibrary/importModelFile.d.ts.map +1 -1
  67. package/dist/assetLibrary/importModelFile.js +3 -16
  68. package/dist/assetLibrary/importModelFiles.d.ts +15 -0
  69. package/dist/assetLibrary/importModelFiles.d.ts.map +1 -0
  70. package/dist/assetLibrary/importModelFiles.js +58 -0
  71. package/dist/assetLibrary/importModelFiles.test.d.ts +2 -0
  72. package/dist/assetLibrary/importModelFiles.test.d.ts.map +1 -0
  73. package/dist/assetLibrary/importModelFiles.test.js +217 -0
  74. package/dist/assetLibrary/importPipeline.d.ts +15 -1
  75. package/dist/assetLibrary/importPipeline.d.ts.map +1 -1
  76. package/dist/assetLibrary/importPipeline.js +74 -38
  77. package/dist/assetLibrary/importPipeline.test.js +25 -0
  78. package/dist/assetLibrary/library.d.ts +1 -0
  79. package/dist/assetLibrary/library.d.ts.map +1 -1
  80. package/dist/assetLibrary/library.js +22 -1
  81. package/dist/assetLibrary/library.test.js +76 -0
  82. package/dist/assetLibrary/seedAssetsManifest.js +1 -1
  83. package/dist/bootstrap.d.ts +2 -1
  84. package/dist/bootstrap.d.ts.map +1 -1
  85. package/dist/bootstrap.js +69 -7
  86. package/dist/bootstrap.test.js +111 -0
  87. package/dist/config.d.ts +10 -1
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/firstScreenGate.d.ts +28 -0
  90. package/dist/firstScreenGate.d.ts.map +1 -0
  91. package/dist/firstScreenGate.js +67 -0
  92. package/dist/firstScreenGate.test.d.ts +2 -0
  93. package/dist/firstScreenGate.test.d.ts.map +1 -0
  94. package/dist/firstScreenGate.test.js +83 -0
  95. package/dist/hostFlows.d.ts +1 -0
  96. package/dist/hostFlows.d.ts.map +1 -1
  97. package/dist/hostFlows.js +9 -0
  98. package/dist/index.d.ts +10 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +6 -1
  101. package/dist/materials/modelMaterials.d.ts +19 -0
  102. package/dist/materials/modelMaterials.d.ts.map +1 -0
  103. package/dist/materials/modelMaterials.js +253 -0
  104. package/dist/materials/modelMaterials.test.d.ts +2 -0
  105. package/dist/materials/modelMaterials.test.d.ts.map +1 -0
  106. package/dist/materials/modelMaterials.test.js +188 -0
  107. package/dist/materials/types.d.ts +20 -0
  108. package/dist/materials/types.d.ts.map +1 -0
  109. package/dist/materials/types.js +1 -0
  110. package/dist/materials/useModelMaterials.d.ts +10 -0
  111. package/dist/materials/useModelMaterials.d.ts.map +1 -0
  112. package/dist/materials/useModelMaterials.js +26 -0
  113. package/dist/materials/useModelMaterials.test.d.ts +2 -0
  114. package/dist/materials/useModelMaterials.test.d.ts.map +1 -0
  115. package/dist/materials/useModelMaterials.test.js +75 -0
  116. package/dist/poses.d.ts +38 -0
  117. package/dist/poses.d.ts.map +1 -0
  118. package/dist/poses.js +41 -0
  119. package/dist/poses.test.d.ts +2 -0
  120. package/dist/poses.test.d.ts.map +1 -0
  121. package/dist/poses.test.js +38 -0
  122. package/dist/primitives.d.ts +5 -0
  123. package/dist/primitives.d.ts.map +1 -0
  124. package/dist/primitives.js +15 -0
  125. package/dist/primitives.test.d.ts +2 -0
  126. package/dist/primitives.test.d.ts.map +1 -0
  127. package/dist/primitives.test.js +27 -0
  128. package/dist/reset.realStudio.test.d.ts +2 -0
  129. package/dist/reset.realStudio.test.d.ts.map +1 -0
  130. package/dist/reset.realStudio.test.js +60 -0
  131. package/dist/studioRoot.d.ts +4 -0
  132. package/dist/studioRoot.d.ts.map +1 -0
  133. package/dist/studioRoot.js +36 -0
  134. package/dist/studioRoot.test.d.ts +2 -0
  135. package/dist/studioRoot.test.d.ts.map +1 -0
  136. package/dist/studioRoot.test.js +62 -0
  137. package/dist/useCameraStudio.d.ts.map +1 -1
  138. package/dist/useCameraStudio.js +3 -2
  139. package/package.json +18 -10
@@ -1,5 +1,5 @@
1
- import { Bone, BoxGeometry, Group, Mesh, MeshBasicMaterial, MeshStandardMaterial, Texture, } from 'three';
2
- import { cloneBoneTree, exportModelGLB, stripInvalidTextures } from './exportGLB';
1
+ import { Bone, Box3, BoxGeometry, Group, Mesh, MeshBasicMaterial, MeshStandardMaterial, Texture, Vector3, } from 'three';
2
+ import { CENTIMETER_SOURCE_THRESHOLD, bakeMetricScale, cloneBoneTree, exportModelGLB, metricScaleFactor, stripInvalidTextures, } from './exportGLB';
3
3
  describe('cloneBoneTree', () => {
4
4
  it('只保留骨骼节点(名字/变换),剔除网格', () => {
5
5
  const root = new Group();
@@ -70,4 +70,58 @@ describe('exportModelGLB', () => {
70
70
  expect(glb.byteLength).toBeGreaterThan(0);
71
71
  expect(mat.map).toBeNull();
72
72
  });
73
+ it('入库时把厘米源换算成米烘进根节点缩放(放置时缩放基线才是 1)', async () => {
74
+ // 模拟 Mixamo FBX 厘米单位:180 单位高的人
75
+ const root = new Group();
76
+ root.add(new Mesh(new BoxGeometry(50, 180, 30), new MeshBasicMaterial()));
77
+ const glb = await exportModelGLB(root);
78
+ expect(glb.byteLength).toBeGreaterThan(0);
79
+ expect(root.scale.x).toBeCloseTo(0.01, 6);
80
+ expect(root.scale.y).toBe(root.scale.x);
81
+ expect(root.scale.z).toBe(root.scale.x);
82
+ });
83
+ });
84
+ describe('metricScaleFactor', () => {
85
+ it('厘米源(超过阈值)换算 0.01,米源不动', () => {
86
+ expect(metricScaleFactor(180)).toBe(0.01);
87
+ expect(metricScaleFactor(1.8)).toBe(1);
88
+ });
89
+ it('阈值边界:恰好阈值按米,超过才按厘米', () => {
90
+ expect(metricScaleFactor(CENTIMETER_SOURCE_THRESHOLD)).toBe(1);
91
+ expect(metricScaleFactor(CENTIMETER_SOURCE_THRESHOLD + 0.01)).toBe(0.01);
92
+ });
93
+ });
94
+ describe('bakeMetricScale', () => {
95
+ it('厘米源烘成米制真实尺寸(180 高的人 → 1.8)', () => {
96
+ const root = new Group();
97
+ root.add(new Mesh(new BoxGeometry(50, 180, 30), new MeshBasicMaterial()));
98
+ expect(bakeMetricScale(root)).toBeCloseTo(0.01, 6);
99
+ expect(root.scale.x).toBeCloseTo(0.01, 6);
100
+ // 烘完最长边 = 1.8 场景单位(米),人高占比不变
101
+ const box = new Box3().setFromObject(root);
102
+ expect(box.getSize(new Vector3()).y).toBeCloseTo(1.8, 6);
103
+ });
104
+ it('米制模型不动(幂等,重烘已烘焙资产系数为 1)', () => {
105
+ const root = new Group();
106
+ root.add(new Mesh(new BoxGeometry(1.8, 1.8, 1.8), new MeshBasicMaterial()));
107
+ expect(bakeMetricScale(root)).toBe(1);
108
+ expect(root.scale.x).toBe(1);
109
+ });
110
+ it('空模型(无边几何时)返回 1 且不动缩放', () => {
111
+ const root = new Group();
112
+ expect(bakeMetricScale(root)).toBe(1);
113
+ expect(root.scale.x).toBe(1);
114
+ });
115
+ it('骨骼局部数据不受影响(动画轨道寻址的那层)', () => {
116
+ const root = new Group();
117
+ const hips = new Bone();
118
+ hips.name = 'Hips';
119
+ hips.position.set(0, 90, 0);
120
+ const mesh = new Mesh(new BoxGeometry(50, 180, 30), new MeshBasicMaterial());
121
+ hips.add(mesh);
122
+ root.add(hips);
123
+ bakeMetricScale(root);
124
+ expect(hips.position.y).toBe(90); // 只有根节点 scale 变了
125
+ expect(root.scale.x).toBeCloseTo(0.01, 6);
126
+ });
73
127
  });
@@ -1,8 +1,9 @@
1
+ import type { LoadingManager } from 'three';
1
2
  import { GLTFLoader } from 'three-stdlib';
2
3
  /**
3
4
  * 统一创建 GLTFLoader:接 Draco 解码(解码器文件在 public/draco,
4
5
  * 从 three 原样拷贝)。未压缩的 GLB 走原路径,不受影响;
5
6
  * DRACOLoader 全局一个,解码 wasm 只加载一次。
6
7
  */
7
- export declare function createGLTFLoader(): GLTFLoader;
8
+ export declare function createGLTFLoader(manager?: LoadingManager): GLTFLoader;
8
9
  //# sourceMappingURL=gltfLoader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gltfLoader.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/gltfLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,UAAU,EAAC,MAAM,cAAc,CAAA;AAIpD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,UAAU,CAG7C"}
1
+ {"version":3,"file":"gltfLoader.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/gltfLoader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,OAAO,CAAA;AACzC,OAAO,EAAc,UAAU,EAAC,MAAM,cAAc,CAAA;AAIpD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,UAAU,CAGrE"}
@@ -5,7 +5,7 @@ let dracoLoader;
5
5
  * 从 three 原样拷贝)。未压缩的 GLB 走原路径,不受影响;
6
6
  * DRACOLoader 全局一个,解码 wasm 只加载一次。
7
7
  */
8
- export function createGLTFLoader() {
8
+ export function createGLTFLoader(manager) {
9
9
  dracoLoader ?? (dracoLoader = new DRACOLoader().setDecoderPath('/draco/'));
10
- return new GLTFLoader().setDRACOLoader(dracoLoader);
10
+ return new GLTFLoader(manager).setDRACOLoader(dracoLoader);
11
11
  }
@@ -0,0 +1,6 @@
1
+ import { LoadingManager } from 'three';
2
+ export declare function createResourceUrlModifier(files: File[]): {
3
+ manager: LoadingManager;
4
+ dispose(): void;
5
+ };
6
+ //# sourceMappingURL=gltfResourceSet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gltfResourceSet.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/gltfResourceSet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,OAAO,CAAA;AAEpC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,IAAI,EAAE;;;EAuBtD"}
@@ -0,0 +1,27 @@
1
+ import { LoadingManager } from 'three';
2
+ export function createResourceUrlModifier(files) {
3
+ const urls = new Map();
4
+ const byName = new Map(files.map((file) => [file.name, file]));
5
+ const manager = new LoadingManager();
6
+ manager.setURLModifier((url) => {
7
+ if (/^(blob:|data:|https?:)/.test(url))
8
+ return url;
9
+ const name = decodeURIComponent(url.split('/').pop() ?? url);
10
+ const file = byName.get(name);
11
+ if (!file)
12
+ throw new Error(`GLTF 外部资源缺失: ${name}`);
13
+ let objectUrl = urls.get(name);
14
+ if (!objectUrl) {
15
+ objectUrl = URL.createObjectURL(file);
16
+ urls.set(name, objectUrl);
17
+ }
18
+ return objectUrl;
19
+ });
20
+ return {
21
+ manager,
22
+ dispose() {
23
+ urls.forEach((url) => URL.revokeObjectURL(url));
24
+ urls.clear();
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gltfResourceSet.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gltfResourceSet.test.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/gltfResourceSet.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { createResourceUrlModifier } from './gltfResourceSet';
2
+ describe('createResourceUrlModifier', () => {
3
+ const originalCreateObjectURL = URL.createObjectURL;
4
+ const originalRevokeObjectURL = URL.revokeObjectURL;
5
+ let nextId = 0;
6
+ beforeEach(() => {
7
+ URL.createObjectURL = jest.fn(() => `blob:resource-${++nextId}`);
8
+ URL.revokeObjectURL = jest.fn();
9
+ });
10
+ afterEach(() => {
11
+ URL.createObjectURL = originalCreateObjectURL;
12
+ URL.revokeObjectURL = originalRevokeObjectURL;
13
+ });
14
+ it('按相对 URI 的文件名映射并复用 object URL,dispose 全部回收', () => {
15
+ const bin = new File([new Uint8Array(36)], 'triangle.bin');
16
+ const png = new File(['png'], 'tiny pixel.png', { type: 'image/png' });
17
+ const resources = createResourceUrlModifier([bin, png]);
18
+ const binUrl = resources.manager.resolveURL('./geometry/triangle.bin');
19
+ const pngUrl = resources.manager.resolveURL('./textures/tiny%20pixel.png');
20
+ expect(resources.manager.resolveURL('triangle.bin')).toBe(binUrl);
21
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
22
+ resources.dispose();
23
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
24
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith(binUrl);
25
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith(pngUrl);
26
+ });
27
+ it('保留已有绝对资源 URL,缺失文件报出资源名', () => {
28
+ const resources = createResourceUrlModifier([]);
29
+ for (const url of [
30
+ 'blob:existing',
31
+ 'data:image/png;base64,x',
32
+ 'https://example.com/a.bin',
33
+ 'http://example.com/a.bin',
34
+ ]) {
35
+ expect(resources.manager.resolveURL(url)).toBe(url);
36
+ }
37
+ expect(() => resources.manager.resolveURL('./missing.bin')).toThrow('GLTF 外部资源缺失: missing.bin');
38
+ resources.dispose();
39
+ });
40
+ });
@@ -1,3 +1,8 @@
1
+ /**
2
+ * 宿主外部模型入库:单文件兼容入口,委托 importModelFiles。
3
+ * 放置管线只认库条目(按 id 解析 blob),外部模型必须真实入库,
4
+ * 姿势/骨架匹配/持久化全部照常。library/deps 可注入(测试用)。
5
+ */
1
6
  import type { ImportDeps } from './importPipeline';
2
7
  import type { Library } from './library';
3
8
  import type { ModelEntry } from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"importModelFile.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/importModelFile.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAEhD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AACtC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,SAAS,CAAA;AAEvC,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,OAAsB,EAC/B,IAAI,GAAE,UAA6B,GAClC,OAAO,CAAC,UAAU,CAAC,CAerB"}
1
+ {"version":3,"file":"importModelFile.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/importModelFile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAGhD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AACtC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,SAAS,CAAA;AAEvC,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,OAAsB,EAC/B,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,UAAU,CAAC,CAGrB"}
@@ -1,19 +1,6 @@
1
- /**
2
- * 宿主外部模型入库:File → 既有导入管线(planImport/executeImport)。
3
- * 放置管线只认库条目(按 id 解析 blob),外部模型必须真实入库,
4
- * 姿势/骨架匹配/持久化全部照常。library/deps 可注入(测试用)。
5
- */
6
- import { planImport, executeImport, realImportDeps } from './importPipeline';
1
+ import { importModelFiles } from './importModelFiles';
7
2
  import { getLibrary } from './library';
8
- export async function importModelFile(file, library = getLibrary(), deps = realImportDeps()) {
9
- const before = new Set((await library.listEntries('model')).map((e) => e.id));
10
- const plan = await planImport([file], library, deps);
11
- // 宿主场景无"相似条目"交互,一律保留为新条目
12
- await executeImport(plan, plan.similar.map(() => 'keep'), library);
13
- const models = await library.listEntries('model');
14
- const created = models.find((m) => !before.has(m.id));
15
- if (!created || created.kind !== 'model') {
16
- throw new Error('[hostFlows] 模型导入失败:未找到新建条目');
17
- }
3
+ export async function importModelFile(file, library = getLibrary(), deps) {
4
+ const [created] = await importModelFiles([file], library, deps);
18
5
  return created;
19
6
  }
@@ -0,0 +1,15 @@
1
+ import { type ImportDeps } from './importPipeline';
2
+ import { type Library } from './library';
3
+ import type { ModelEntry } from './types';
4
+ export type ImportFileFailure = {
5
+ file: string;
6
+ message: string;
7
+ };
8
+ export declare class ImportModelFilesError extends Error {
9
+ readonly entries: ModelEntry[];
10
+ readonly failedItems: ImportFileFailure[];
11
+ readonly cause: unknown;
12
+ constructor(entries: ModelEntry[], failedItems: ImportFileFailure[], cause: unknown);
13
+ }
14
+ export declare function importModelFiles(files: File[], library?: Library, deps?: ImportDeps): Promise<ModelEntry[]>;
15
+ //# sourceMappingURL=importModelFiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importModelFiles.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/importModelFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAa,KAAK,OAAO,EAAC,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,SAAS,CAAA;AAEvC,MAAM,MAAM,iBAAiB,GAAG;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAAA;AAE/D,qBAAa,qBAAsB,SAAQ,KAAK;IAE5C,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE;IAC9B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,EAAE;IACzC,QAAQ,CAAC,KAAK,EAAE,OAAO;gBAFd,OAAO,EAAE,UAAU,EAAE,EACrB,WAAW,EAAE,iBAAiB,EAAE,EAChC,KAAK,EAAE,OAAO;CAS1B;AASD,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,GAAE,OAAsB,EAC/B,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CAkDvB"}
@@ -0,0 +1,58 @@
1
+ import { executeImport, ImportItemWriteError, planImport, realImportDeps, } from './importPipeline';
2
+ import { getLibrary } from './library';
3
+ export class ImportModelFilesError extends Error {
4
+ constructor(entries, failedItems, cause) {
5
+ super(`模型导入部分失败: ${failedItems
6
+ .map((item) => `${item.file}: ${item.message}`)
7
+ .join('; ')}`);
8
+ this.entries = entries;
9
+ this.failedItems = failedItems;
10
+ this.cause = cause;
11
+ this.name = 'ImportModelFilesError';
12
+ }
13
+ }
14
+ function failureOf(item) {
15
+ return {
16
+ file: item.sourceFile ?? item.name,
17
+ message: item.type === 'skip' ? item.reason : '写入失败',
18
+ };
19
+ }
20
+ export async function importModelFiles(files, library = getLibrary(), deps) {
21
+ const before = new Set((await library.listEntries('model')).map((e) => e.id));
22
+ const modelFiles = files.filter((file) => /\.(glb|gltf|fbx)$/i.test(file.name));
23
+ const plan = await planImport(modelFiles, library, deps ?? realImportDeps(files));
24
+ const failedItems = plan.items
25
+ .filter((item) => item.type === 'skip' && !item.duplicate)
26
+ .map(failureOf);
27
+ let executeError;
28
+ try {
29
+ await executeImport(plan, plan.similar.map(() => 'keep'), library);
30
+ }
31
+ catch (error) {
32
+ executeError = error;
33
+ if (error instanceof ImportItemWriteError) {
34
+ const cause = error.cause;
35
+ failedItems.push({
36
+ ...failureOf(error.item),
37
+ message: cause instanceof Error ? cause.message : String(cause),
38
+ });
39
+ }
40
+ else {
41
+ failedItems.push({
42
+ file: 'unknown',
43
+ message: error instanceof Error ? error.message : String(error),
44
+ });
45
+ }
46
+ }
47
+ const entries = (await library.listEntries('model')).filter((entry) => entry.kind === 'model' && !before.has(entry.id));
48
+ if (executeError || failedItems.length > 0) {
49
+ const cause = executeError instanceof ImportItemWriteError
50
+ ? executeError.cause
51
+ : executeError;
52
+ throw new ImportModelFilesError(entries, failedItems, cause);
53
+ }
54
+ if (entries.length === 0) {
55
+ throw new Error('[hostFlows] 模型导入失败:未找到新建条目');
56
+ }
57
+ return entries;
58
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=importModelFiles.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importModelFiles.test.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/importModelFiles.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,217 @@
1
+ import { BoxGeometry, Group, Mesh, MeshBasicMaterial } from 'three';
2
+ import { importModelFiles } from './importModelFiles';
3
+ import { Library } from './library';
4
+ const tinyPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
5
+ function triangleGltf() {
6
+ const document = {
7
+ asset: { version: '2.0' },
8
+ scene: 0,
9
+ scenes: [{ nodes: [0] }],
10
+ nodes: [{ mesh: 0 }],
11
+ meshes: [
12
+ {
13
+ primitives: [{ attributes: { POSITION: 0 }, material: 0, mode: 4 }],
14
+ },
15
+ ],
16
+ materials: [
17
+ {
18
+ pbrMetallicRoughness: {
19
+ baseColorTexture: { index: 0 },
20
+ metallicFactor: 0,
21
+ roughnessFactor: 1,
22
+ },
23
+ },
24
+ ],
25
+ textures: [{ source: 0 }],
26
+ images: [{ uri: 'textures/tiny.png' }],
27
+ buffers: [{ uri: 'geometry/triangle.bin', byteLength: 36 }],
28
+ bufferViews: [{ buffer: 0, byteOffset: 0, byteLength: 36 }],
29
+ accessors: [
30
+ {
31
+ bufferView: 0,
32
+ componentType: 5126,
33
+ count: 3,
34
+ type: 'VEC3',
35
+ min: [0, 0, 0],
36
+ max: [1, 1, 0],
37
+ },
38
+ ],
39
+ };
40
+ return new File([JSON.stringify(document)], 'triangle.gltf', {
41
+ type: 'model/gltf+json',
42
+ });
43
+ }
44
+ function triangleBin() {
45
+ const bytes = new ArrayBuffer(36);
46
+ const positions = new Float32Array(bytes);
47
+ positions.set([0, 0, 0, 1, 0, 0, 0, 1, 0]);
48
+ return new File([bytes], 'triangle.bin', {
49
+ type: 'application/octet-stream',
50
+ });
51
+ }
52
+ function tinyPngFile() {
53
+ return new File([tinyPng], 'tiny.png', { type: 'image/png' });
54
+ }
55
+ function memoryBackend() {
56
+ const entries = new Map();
57
+ const blobs = new Map();
58
+ const backend = {
59
+ id: 'mock',
60
+ listEntries: async (kind) => [...entries.values()].filter((entry) => !kind || entry.kind === kind),
61
+ getEntry: async (id) => entries.get(id),
62
+ putEntry: async (entry) => void entries.set(entry.id, entry),
63
+ deleteEntry: async (id) => void entries.delete(id),
64
+ putBlob: async (blob) => {
65
+ const id = `blob-${blobs.size}`;
66
+ blobs.set(id, blob);
67
+ return id;
68
+ },
69
+ getBlobUrl: async () => 'blob:stored',
70
+ deleteBlob: async (id) => void blobs.delete(id),
71
+ listBlobIds: async () => [...blobs.keys()],
72
+ getRigAliases: async () => [],
73
+ putRigAlias: async () => { },
74
+ };
75
+ return { backend, entries, blobs };
76
+ }
77
+ function modelDeps(names) {
78
+ return {
79
+ load: async (file) => {
80
+ if (!names.includes(file.name))
81
+ throw new Error('无法解析');
82
+ const root = new Group();
83
+ root.name = file.name;
84
+ root.add(new Mesh(new BoxGeometry(), new MeshBasicMaterial()));
85
+ return { root, animations: [] };
86
+ },
87
+ exportModel: async (root) => new TextEncoder().encode(root.name).buffer,
88
+ exportAnimation: async () => new TextEncoder().encode('anim').buffer,
89
+ };
90
+ }
91
+ function gltfJsonChunk(glb) {
92
+ const view = new DataView(glb);
93
+ const jsonLength = view.getUint32(12, true);
94
+ expect(view.getUint32(16, true)).toBe(0x4e4f534a);
95
+ return JSON.parse(new TextDecoder().decode(new Uint8Array(glb, 20, jsonLength)));
96
+ }
97
+ describe('importModelFiles', () => {
98
+ const originalFetch = globalThis.fetch;
99
+ const originalSelf = globalThis.self;
100
+ const originalCreateImageBitmap = globalThis.createImageBitmap;
101
+ const originalImageData = globalThis.ImageData;
102
+ const originalCreateObjectURL = URL.createObjectURL;
103
+ const originalRevokeObjectURL = URL.revokeObjectURL;
104
+ const originalCreateElement = document.createElement;
105
+ let objectUrls;
106
+ beforeEach(() => {
107
+ globalThis.self = globalThis;
108
+ let nextId = 0;
109
+ objectUrls = new Map();
110
+ URL.createObjectURL = jest.fn((blob) => {
111
+ const url = `blob:test-${++nextId}`;
112
+ objectUrls.set(url, blob);
113
+ return url;
114
+ });
115
+ URL.revokeObjectURL = jest.fn((url) => {
116
+ objectUrls.delete(url);
117
+ });
118
+ globalThis.fetch = jest.fn(async (input) => {
119
+ const url = typeof input === 'string' || input instanceof URL
120
+ ? String(input)
121
+ : input.url;
122
+ const blob = objectUrls.get(url);
123
+ if (!blob)
124
+ throw new Error(`未登记的测试 URL: ${url}`);
125
+ return {
126
+ ok: true,
127
+ status: 200,
128
+ statusText: 'OK',
129
+ url,
130
+ headers: { get: () => null },
131
+ arrayBuffer: () => blob.arrayBuffer(),
132
+ blob: async () => blob,
133
+ };
134
+ });
135
+ globalThis.createImageBitmap = jest.fn(async () => ({
136
+ data: new Uint8ClampedArray([255, 255, 255, 255]),
137
+ width: 1,
138
+ height: 1,
139
+ close: jest.fn(),
140
+ }));
141
+ globalThis.ImageData = class {
142
+ constructor(data, width, height) {
143
+ this.data = data;
144
+ this.width = width;
145
+ this.height = height;
146
+ }
147
+ };
148
+ document.createElement = jest.fn(() => ({
149
+ width: 0,
150
+ height: 0,
151
+ getContext: () => ({
152
+ translate: jest.fn(),
153
+ scale: jest.fn(),
154
+ drawImage: jest.fn(),
155
+ putImageData: jest.fn(),
156
+ }),
157
+ toBlob: (callback) => callback(new Blob([tinyPng], { type: 'image/png' })),
158
+ }));
159
+ });
160
+ afterEach(() => {
161
+ globalThis.fetch = originalFetch;
162
+ globalThis.self = originalSelf;
163
+ globalThis.createImageBitmap = originalCreateImageBitmap;
164
+ globalThis.ImageData = originalImageData;
165
+ URL.createObjectURL = originalCreateObjectURL;
166
+ URL.revokeObjectURL = originalRevokeObjectURL;
167
+ if (originalCreateElement)
168
+ document.createElement = originalCreateElement;
169
+ else
170
+ Reflect.deleteProperty(document, 'createElement');
171
+ });
172
+ it('把相对 buffer/image 一起装入一个自包含 GLB,并回收全部临时 URL', async () => {
173
+ const { backend, entries, blobs } = memoryBackend();
174
+ const library = new Library({ sources: [backend] });
175
+ const imported = await importModelFiles([triangleGltf(), triangleBin(), tinyPngFile()], library);
176
+ expect(imported).toHaveLength(1);
177
+ expect(imported[0]).toMatchObject({
178
+ kind: 'model',
179
+ name: 'triangle',
180
+ vertexCount: 3,
181
+ });
182
+ expect(entries.size).toBe(1);
183
+ const glb = await blobs.get(imported[0].blobId).arrayBuffer();
184
+ expect(new DataView(glb).getUint32(0, true)).toBe(0x46546c67);
185
+ const json = gltfJsonChunk(glb);
186
+ expect(json.buffers[0].uri).toBeUndefined();
187
+ expect(json.images[0].uri).toBeUndefined();
188
+ expect(json.images[0].bufferView).toEqual(expect.any(Number));
189
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
190
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
191
+ expect(objectUrls.size).toBe(0);
192
+ });
193
+ it('引用的 bin 缺失时不创建任何新条目,并回收已创建的 URL', async () => {
194
+ const { backend, entries } = memoryBackend();
195
+ const library = new Library({ sources: [backend] });
196
+ await expect(importModelFiles([triangleGltf(), tinyPngFile()], library)).rejects.toThrow();
197
+ expect(entries.size).toBe(0);
198
+ expect(objectUrls.size).toBe(0);
199
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith(expect.stringMatching(/^blob:test-/));
200
+ });
201
+ it('多个模型根全部成功时按原数组契约返回全部新条目', async () => {
202
+ const { backend } = memoryBackend();
203
+ const library = new Library({ sources: [backend] });
204
+ const imported = await importModelFiles([new File(['a'], 'a.gltf'), new File(['b'], 'b.gltf')], library, modelDeps(['a.gltf', 'b.gltf']));
205
+ expect(imported.map((entry) => entry.name)).toEqual(['a', 'b']);
206
+ expect(await library.listEntries('model')).toHaveLength(2);
207
+ });
208
+ it('混合成功时保留成功条目并报告失败文件及原因', async () => {
209
+ const { backend } = memoryBackend();
210
+ const library = new Library({ sources: [backend] });
211
+ await expect(importModelFiles([new File(['a'], 'a.gltf'), new File(['x'], 'broken.gltf')], library, modelDeps(['a.gltf']))).rejects.toMatchObject({
212
+ entries: [expect.objectContaining({ kind: 'model', name: 'a' })],
213
+ failedItems: [{ file: 'broken.gltf', message: expect.any(String) }],
214
+ });
215
+ expect((await library.listEntries('model')).map((entry) => entry.name)).toEqual(['a']);
216
+ });
217
+ });
@@ -12,10 +12,11 @@ export type ImportDeps = {
12
12
  exportModel: (root: Object3D) => Promise<ArrayBuffer>;
13
13
  exportAnimation: (root: Object3D, clip: AnimationClip) => Promise<ArrayBuffer>;
14
14
  };
15
- export declare const realImportDeps: () => ImportDeps;
15
+ export declare const realImportDeps: (files?: File[]) => ImportDeps;
16
16
  export type ImportItem = {
17
17
  type: 'model';
18
18
  name: string;
19
+ sourceFile?: string;
19
20
  rig: string;
20
21
  contentHash: string;
21
22
  boneCount: number;
@@ -24,6 +25,7 @@ export type ImportItem = {
24
25
  } | {
25
26
  type: 'animation';
26
27
  name: string;
28
+ sourceFile?: string;
27
29
  rig: string;
28
30
  contentHash: string;
29
31
  duration: number;
@@ -31,12 +33,15 @@ export type ImportItem = {
31
33
  } | {
32
34
  type: 'pose';
33
35
  name: string;
36
+ sourceFile?: string;
34
37
  rig: string;
35
38
  bones: PoseEntry['bones'];
36
39
  } | {
37
40
  type: 'skip';
38
41
  name: string;
42
+ sourceFile?: string;
39
43
  reason: string;
44
+ duplicate?: boolean;
40
45
  };
41
46
  /** 哈希不同但同族同结构的模型,入库前要用户选 reuse/keep */
42
47
  export type SimilarMatch = {
@@ -56,6 +61,15 @@ export declare function baseName(fileName: string): string;
56
61
  * 第二层:哈希不同但同族 + 顶点/骨骼数相同 → 记入 similar 交给用户。
57
62
  */
58
63
  export declare function planImport(files: File[], library: Pick<Library, 'listEntries'>, deps: ImportDeps, tr?: TFunction): Promise<ImportPlan>;
64
+ export declare class ImportItemWriteError extends Error {
65
+ readonly item: Exclude<ImportItem, {
66
+ type: 'skip';
67
+ }>;
68
+ readonly cause: unknown;
69
+ constructor(item: Exclude<ImportItem, {
70
+ type: 'skip';
71
+ }>, cause: unknown);
72
+ }
59
73
  /** 落库:choices 与 plan.similar 一一对应,'reuse' 丢弃对应 model 项 */
60
74
  export declare function executeImport(plan: ImportPlan, choices: Array<'reuse' | 'keep'>, library: Library): Promise<void>;
61
75
  //# sourceMappingURL=importPipeline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"importPipeline.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/importPipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;AACzD,OAAO,EAAY,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAA;AAKlE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAEtC,OAAO,KAAK,EAA6B,SAAS,EAAC,MAAM,SAAS,CAAA;AAElE,MAAM,MAAM,WAAW,GAAG;IAAC,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAC,CAAA;AAEpE,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACrD,eAAe,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;CAC/E,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,UAIjC,CAAA;AAmCF,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,WAAW,CAAA;CACjB,GACD;IACE,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,WAAW,CAAA;CACjB,GACD;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;CAAC,GACpE;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAA;AAEhD,uCAAuC;AACvC,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEjD;AAuBD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EACrC,IAAI,EAAE,UAAU,EAChB,EAAE,GAAE,SAAqB,GACxB,OAAO,CAAC,UAAU,CAAC,CAsJrB;AAED,0DAA0D;AAC1D,wBAAsB,aAAa,CACjC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,EAChC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAgDf"}
1
+ {"version":3,"file":"importPipeline.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/importPipeline.ts"],"names":[],"mappings":"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"}