@forgeax-extension/character-3d 0.1.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.
Files changed (110) hide show
  1. package/SOURCE_PROVENANCE.md +8 -0
  2. package/dist/assets/index-CIx3aXo1.js +4336 -0
  3. package/dist/assets/index-CIx3aXo1.js.map +1 -0
  4. package/dist/assets/index-CmhOHLEB.css +1 -0
  5. package/dist/hdr/README.md +28 -0
  6. package/dist/hdr/presets.json +8 -0
  7. package/dist/index.html +13 -0
  8. package/forgeax-extension.json +428 -0
  9. package/marketplace-card.json +56 -0
  10. package/package.json +59 -0
  11. package/packages/external-asset-meta/package.json +24 -0
  12. package/packages/external-asset-meta/src/cook.test.ts +211 -0
  13. package/packages/external-asset-meta/src/cook.ts +188 -0
  14. package/packages/external-asset-meta/src/draco3dgltf.d.ts +11 -0
  15. package/packages/external-asset-meta/src/index.ts +18 -0
  16. package/packages/external-asset-meta/src/normalize.test.ts +36 -0
  17. package/packages/external-asset-meta/src/normalize.ts +101 -0
  18. package/packages/external-asset-meta/src/types.ts +82 -0
  19. package/schemas/adopt-playable-character.args.json +26 -0
  20. package/schemas/adopt-playable-character.returns.json +31 -0
  21. package/schemas/apply-motion.args.json +47 -0
  22. package/schemas/apply-motion.returns.json +23 -0
  23. package/schemas/auto-rig.args.json +35 -0
  24. package/schemas/auto-rig.returns.json +23 -0
  25. package/schemas/delete-asset.args.json +18 -0
  26. package/schemas/delete-asset.returns.json +18 -0
  27. package/schemas/engine-import-status.args.json +18 -0
  28. package/schemas/engine-import-status.returns.json +50 -0
  29. package/schemas/export-playable-character.args.json +21 -0
  30. package/schemas/export-playable-character.returns.json +47 -0
  31. package/schemas/gen3d-asset-manifest.json +281 -0
  32. package/schemas/generate-meshy-text-mock.args.json +42 -0
  33. package/schemas/generate-meshy-text-mock.returns.json +301 -0
  34. package/schemas/get-credentials.args.json +7 -0
  35. package/schemas/get-credentials.returns.json +45 -0
  36. package/schemas/get-playable-profile.args.json +11 -0
  37. package/schemas/get-playable-profile.returns.json +188 -0
  38. package/schemas/image-to-3d.args.json +47 -0
  39. package/schemas/image-to-3d.returns.json +306 -0
  40. package/schemas/import-to-engine.args.json +18 -0
  41. package/schemas/import-to-engine.returns.json +35 -0
  42. package/schemas/list-assets.args.json +22 -0
  43. package/schemas/list-assets.returns.json +296 -0
  44. package/schemas/list-motions.args.json +29 -0
  45. package/schemas/list-motions.returns.json +41 -0
  46. package/schemas/pose-standardization.args.json +28 -0
  47. package/schemas/pose-standardization.returns.json +29 -0
  48. package/schemas/provider-status.args.json +7 -0
  49. package/schemas/provider-status.returns.json +70 -0
  50. package/schemas/refine-mesh.args.json +33 -0
  51. package/schemas/refine-mesh.returns.json +306 -0
  52. package/schemas/rename-asset.args.json +22 -0
  53. package/schemas/rename-asset.returns.json +14 -0
  54. package/schemas/retopo-lowpoly.args.json +34 -0
  55. package/schemas/retopo-lowpoly.returns.json +24 -0
  56. package/schemas/score-quality.args.json +36 -0
  57. package/schemas/score-quality.returns.json +12 -0
  58. package/schemas/set-credentials.args.json +32 -0
  59. package/schemas/set-credentials.returns.json +45 -0
  60. package/schemas/set-playable-motion-mapping.args.json +25 -0
  61. package/schemas/set-playable-motion-mapping.returns.json +33 -0
  62. package/schemas/set-playable-profile.args.json +32 -0
  63. package/schemas/set-playable-profile.returns.json +67 -0
  64. package/schemas/text-to-3d.args.json +51 -0
  65. package/schemas/text-to-3d.returns.json +306 -0
  66. package/schemas/upload-image.args.json +19 -0
  67. package/schemas/upload-image.returns.json +21 -0
  68. package/schemas/upload-video.args.json +14 -0
  69. package/schemas/upload-video.returns.json +14 -0
  70. package/schemas/views-to-3d.args.json +58 -0
  71. package/schemas/views-to-3d.returns.json +306 -0
  72. package/server/adopt-playable-character.ts +291 -0
  73. package/server/asset-storage.ts +136 -0
  74. package/server/asset-upload.ts +123 -0
  75. package/server/audit.ts +58 -0
  76. package/server/cache.ts +75 -0
  77. package/server/cos-uploader.ts +141 -0
  78. package/server/credentials-store.ts +186 -0
  79. package/server/engine-import.ts +238 -0
  80. package/server/env.ts +287 -0
  81. package/server/export-playable-character.ts +364 -0
  82. package/server/generate.ts +89 -0
  83. package/server/merge-playable-character.ts +259 -0
  84. package/server/motion-catalog.ts +137 -0
  85. package/server/per-game-store.ts +1016 -0
  86. package/server/providers/gateway-client.ts +171 -0
  87. package/server/providers/gateway-data.ts +182 -0
  88. package/server/providers/gateway-models.ts +49 -0
  89. package/server/providers/hunyuan-rest.ts +317 -0
  90. package/server/providers/hunyuan-workflow.ts +225 -0
  91. package/server/providers/meshy-direct-client.ts +223 -0
  92. package/server/providers/meshy.ts +548 -0
  93. package/server/providers/rodin-gateway.ts +193 -0
  94. package/server/providers/rodin.ts +253 -0
  95. package/server/providers/visvise.ts +329 -0
  96. package/server/rate-guard.ts +29 -0
  97. package/server/tool-handlers.ts +2193 -0
  98. package/shared/catalog.ts +307 -0
  99. package/shared/manifest.ts +378 -0
  100. package/shared/meshy-actions.ts +690 -0
  101. package/shared/playable-preview-url.test.ts +14 -0
  102. package/shared/playable-preview-url.ts +13 -0
  103. package/shared/playable-profile.test.ts +127 -0
  104. package/shared/playable-profile.ts +171 -0
  105. package/shared/provider-params.test.ts +172 -0
  106. package/shared/provider-params.ts +389 -0
  107. package/shared/quality/heuristics.test.ts +84 -0
  108. package/shared/quality/heuristics.ts +100 -0
  109. package/shared/rodin-image.test.ts +51 -0
  110. package/shared/rodin-image.ts +125 -0
@@ -0,0 +1,307 @@
1
+ // Provider capability catalog + ProviderResult contract.
2
+ //
3
+ // catalog = static, source-derived planning data (which provider/mode is safe
4
+ // to expose). It is NOT a benchmark/scoring runtime — provider-comparison
5
+ // conclusions live in docs only (ADR-0001). Pure data + pure functions; no
6
+ // filesystem, no network. Persistence orchestration lives in server/.
7
+
8
+ export type Exposure = 'planned' | 'mock-first' | 'experimental' | 'hidden' | 'blocked';
9
+
10
+ import type {
11
+ FileFormat,
12
+ FileRole,
13
+ GenerationMode,
14
+ ProviderId,
15
+ } from './manifest';
16
+
17
+ export interface ProviderCapability {
18
+ providerId: ProviderId;
19
+ providerName: string;
20
+ capability: string;
21
+ sourceStatus: string;
22
+ exposure: Exposure;
23
+ notes: string;
24
+ }
25
+
26
+ export type PromptCategory = 'character' | 'prop' | 'scene';
27
+
28
+ // ProviderResult — the pure output of a provider adapter. A provider knows
29
+ // nothing about cache or asset-store (ADR-0001 decoupling). It returns the bytes
30
+ // (already downloaded from any provider URL) plus role/format so the handler can
31
+ // persist them. `sourceJobId` is audit-only, never a stored asset reference.
32
+ export interface ProviderResultFile {
33
+ role: FileRole;
34
+ format: FileFormat;
35
+ data: Uint8Array;
36
+ }
37
+
38
+ export interface MeshyTaskRefs {
39
+ /** Text preview (white mesh) task id — only valid input to refine. */
40
+ previewTaskId: string | null;
41
+ /** Final usable mesh task id (refine after auto-texture, else the single submit). */
42
+ resultTaskId: string | null;
43
+ }
44
+
45
+ /** VISVISE COS URLs from generate/rig. Downstream tools must reuse these. */
46
+ export interface VisviseRefs {
47
+ meshUrl: string | null;
48
+ rigUrl: string | null;
49
+ }
50
+
51
+ export interface ProviderResult {
52
+ provider: ProviderId;
53
+ mode: GenerationMode;
54
+ providerMode: 'mock' | 'real';
55
+ sourceJobId: string | null;
56
+ prompt: string | null;
57
+ files: ProviderResultFile[];
58
+ /** Meshy-only preview/result task pair (absent for other providers). */
59
+ meshyTaskRefs?: MeshyTaskRefs;
60
+ /** VISVISE-only generate/rig COS URLs (absent for other providers). */
61
+ visviseRefs?: VisviseRefs;
62
+ }
63
+
64
+ export interface MeshyTextMockArgs {
65
+ prompt: string;
66
+ promptCategory?: PromptCategory;
67
+ enablePbr?: boolean;
68
+ targetPolycount?: number;
69
+ }
70
+
71
+ export const QUALITY_RUBRIC = [
72
+ 'geometry',
73
+ 'topology',
74
+ 'texture',
75
+ 'pbr',
76
+ 'prompt_fidelity',
77
+ ] as const;
78
+
79
+ export const CAPABILITIES: readonly ProviderCapability[] = [
80
+ {
81
+ providerId: 'meshy',
82
+ providerName: 'Meshy',
83
+ capability: 'text / text-to-3D preview',
84
+ sourceStatus: 'Implemented in lab',
85
+ exposure: 'mock-first',
86
+ notes: 'Good M2/M3 candidate. Preserve cache-first behavior before enabling real calls.',
87
+ },
88
+ {
89
+ providerId: 'meshy',
90
+ providerName: 'Meshy',
91
+ capability: 'image / image-to-3D',
92
+ sourceStatus: 'Implemented in lab',
93
+ exposure: 'planned',
94
+ notes: 'Requires image input handling and artifact display.',
95
+ },
96
+ {
97
+ providerId: 'meshy',
98
+ providerName: 'Meshy',
99
+ capability: 'views / multi-image-to-3D',
100
+ sourceStatus: 'Implemented in lab',
101
+ exposure: 'planned',
102
+ notes: 'Meshy accepts image URL arrays rather than Hunyuan named view slots.',
103
+ },
104
+ {
105
+ providerId: 'meshy',
106
+ providerName: 'Meshy',
107
+ capability: 'refine',
108
+ sourceStatus: 'Implemented in lab',
109
+ exposure: 'planned',
110
+ notes: 'Meshy-only second stage; keep provider-specific.',
111
+ },
112
+ {
113
+ providerId: 'hunyuan_workflow',
114
+ providerName: 'Hunyuan workflow',
115
+ capability: 'text',
116
+ sourceStatus: 'Verified main mode',
117
+ exposure: 'planned',
118
+ notes: 'Use hunyuan-3d-v3.1-text2gen-wf with async submit and poll.',
119
+ },
120
+ {
121
+ providerId: 'hunyuan_workflow',
122
+ providerName: 'Hunyuan workflow',
123
+ capability: 'image',
124
+ sourceStatus: 'Verified main mode',
125
+ exposure: 'planned',
126
+ notes: 'Use hunyuan-3d-v3.1-image2gen-wf with async submit and poll.',
127
+ },
128
+ {
129
+ providerId: 'hunyuan_workflow',
130
+ providerName: 'Hunyuan workflow',
131
+ capability: 'views',
132
+ sourceStatus: 'Verified main mode',
133
+ exposure: 'planned',
134
+ notes: 'Use hunyuan-3d-v3.1-views2gen-wf and Hunyuan named view slots.',
135
+ },
136
+ {
137
+ providerId: 'hunyuan_workflow',
138
+ providerName: 'Hunyuan workflow',
139
+ capability: 'geometry and world workflow modes',
140
+ sourceStatus: 'Endpoint reachable, field sanity incomplete',
141
+ exposure: 'hidden',
142
+ notes: 'Keep out of UI and schemas until output shape is verified.',
143
+ },
144
+ {
145
+ providerId: 'hunyuan_rest',
146
+ providerName: 'Hunyuan REST',
147
+ capability: 'pose_standardization',
148
+ sourceStatus: 'Implemented + live-verified 2026-06-10',
149
+ exposure: 'mock-first',
150
+ notes:
151
+ 'Tool gen3d:pose-standardization. Synchronous REST; image to A/T-pose standardization (upstream preprocessing, persists a blob, no manifest). Real call confirmed ~20s. Falls back to mock when GEN3D_ENABLE_REAL_PROVIDERS≠1.',
152
+ },
153
+ {
154
+ providerId: 'hunyuan_rest',
155
+ providerName: 'Hunyuan REST',
156
+ capability: 'low_poly',
157
+ sourceStatus: 'Contract from internal PDF; not live-tested in gen3d',
158
+ exposure: 'planned',
159
+ notes:
160
+ 'M13-1 gen3d:retopo-lowpoly. Async submit/poll (hunyuan-3d-low-poly-v1.5). High-poly source retained by default (2026-06-12). Gate 0 verifies Hunyuan can fetch public COS model URLs.',
161
+ },
162
+ {
163
+ providerId: 'hunyuan_rest',
164
+ providerName: 'Hunyuan REST',
165
+ capability: 'motion_retarget v1',
166
+ sourceStatus: 'Verified end-to-end in lab',
167
+ exposure: 'planned',
168
+ notes:
169
+ 'M13-3 gen3d:apply-motion. int motion types 9-16 (跨步/摔倒/跳跃/踢腿/挥击/步行/跑步/跳舞, decided 2026-06-12). Input = rigged humanoid FBX (role=rigged_model).',
170
+ },
171
+ {
172
+ providerId: 'hunyuan_rest',
173
+ providerName: 'Hunyuan REST',
174
+ capability: 'auto_rigging',
175
+ sourceStatus: 'Endpoint reachable, not fully verified',
176
+ exposure: 'experimental',
177
+ notes: 'M13-2 gen3d:auto-rig. Gate 1 verifies end-to-end output before default AI exposure.',
178
+ },
179
+ {
180
+ providerId: 'hunyuan_rest',
181
+ providerName: 'Hunyuan REST',
182
+ capability: 'motion_retarget_v2',
183
+ sourceStatus: 'Blocked by unknown literal list',
184
+ exposure: 'blocked',
185
+ notes: 'Endpoint may return 200 while falling back to default motion.',
186
+ },
187
+ {
188
+ providerId: 'rodin',
189
+ providerName: 'Rodin',
190
+ capability: 'text / text-to-3D',
191
+ sourceStatus: 'On LiteLLM as rodin-3d (2026-08-14 C1). getRodinEnv uses the shared 3D key. Old Hyper3D rodin.ts is unused.',
192
+ exposure: 'mock-first',
193
+ notes: 'Gateway POST /v1/3d/generations model=rodin-3d, body {prompt}. Do not read RODIN_API_KEY.',
194
+ },
195
+ {
196
+ providerId: 'rodin',
197
+ providerName: 'Rodin',
198
+ capability: 'image / image-to-3D',
199
+ sourceStatus: 'On LiteLLM as rodin-3d (2026-08-14 C1).',
200
+ exposure: 'mock-first',
201
+ notes: 'Gateway body {image_url} or {image_urls}. Static GLB only — no Rodin pose/rig.',
202
+ },
203
+ {
204
+ providerId: 'rodin',
205
+ providerName: 'Rodin',
206
+ capability: 'views / multi-image-to-3D',
207
+ sourceStatus: 'Same LiteLLM rodin-3d as image; body {image_urls} (2–5).',
208
+ exposure: 'mock-first',
209
+ notes: 'views-to-3d sends front/back/left/right as image_urls. First image is used for material. No separate views model.',
210
+ },
211
+ {
212
+ providerId: 'visvise',
213
+ providerName: 'VISVISE',
214
+ capability: 'text / image to 3D + pose + rig + text/video motion',
215
+ sourceStatus: 'On LiteLLM 2026-08-14 (visvise-3d / mid / low / image-pose / rigging / text-motion / video-motion).',
216
+ exposure: 'mock-first',
217
+ notes:
218
+ 'Do not invent algorithm_model. visvise-3d-mid is image-only. input_model_url for pose/rig/motion. No views model. Skinning not chained unless live proves needed.',
219
+ },
220
+ ];
221
+
222
+ export function clampTargetPolycount(value: number): number {
223
+ if (!Number.isFinite(value)) return 30000;
224
+ return Math.min(300000, Math.max(1000, Math.round(value)));
225
+ }
226
+
227
+ // Cache key = stable hash of (provider, mode, normalized inputs). Mirrors the
228
+ // lab's "provider-mode + sorted non-empty payload" dedup (see ADR-0001 Cache).
229
+ export function makeCacheKey(
230
+ provider: ProviderId,
231
+ mode: GenerationMode,
232
+ payload: Record<string, string | number | boolean>,
233
+ ): string {
234
+ const normalized = Object.keys(payload)
235
+ .filter((key) => payload[key] !== '' && payload[key] !== undefined)
236
+ .sort()
237
+ .map((key) => `${key}:${String(payload[key])}`)
238
+ .join('|');
239
+ return `${provider}-${mode}-${fnv1a(`${provider}|${mode}|${normalized}`)}`;
240
+ }
241
+
242
+ // Deterministic no-quota Meshy text mock. Produces a ProviderResult with a small
243
+ // placeholder GLB byte buffer + a preview image so the handler can exercise the
244
+ // full persistence path (blob → manifest) without any remote call.
245
+ export function generateMeshyTextMockResult(args: MeshyTextMockArgs): {
246
+ cacheKey: string;
247
+ result: ProviderResult;
248
+ } {
249
+ const prompt = args.prompt.trim();
250
+ if (!prompt) {
251
+ throw Object.assign(new Error('prompt is required'), { code: 'invalid_prompt' });
252
+ }
253
+ const promptCategory = args.promptCategory ?? 'prop';
254
+ const enablePbr = args.enablePbr ?? true;
255
+ const targetPolycount = clampTargetPolycount(args.targetPolycount ?? 30000);
256
+ const cacheKey = makeCacheKey('meshy', 'text', {
257
+ prompt,
258
+ promptCategory,
259
+ enablePbr,
260
+ targetPolycount,
261
+ });
262
+
263
+ return {
264
+ cacheKey,
265
+ result: {
266
+ provider: 'meshy',
267
+ mode: 'text',
268
+ providerMode: 'mock',
269
+ sourceJobId: `mock-${cacheKey}`,
270
+ prompt,
271
+ files: [
272
+ { role: 'source_mesh', format: 'glb', data: mockGlbBytes(cacheKey) },
273
+ { role: 'preview_image', format: 'png', data: MOCK_PNG_BYTES },
274
+ ],
275
+ },
276
+ };
277
+ }
278
+
279
+ // Minimal valid-ish GLB header (magic "glTF" + version 2) padded with a
280
+ // deterministic tail. Not a renderable model — a stand-in byte payload so the
281
+ // store/manifest path runs end-to-end without a provider call.
282
+ function mockGlbBytes(seed: string): Uint8Array {
283
+ const header = new Uint8Array([0x67, 0x6c, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00]);
284
+ const tail = new TextEncoder().encode(`mock-glb:${seed}`);
285
+ const out = new Uint8Array(header.length + tail.length);
286
+ out.set(header, 0);
287
+ out.set(tail, header.length);
288
+ return out;
289
+ }
290
+
291
+ // 1x1 transparent PNG.
292
+ const MOCK_PNG_BYTES = Uint8Array.from([
293
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
294
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
295
+ 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
296
+ 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
297
+ 0x42, 0x60, 0x82,
298
+ ]);
299
+
300
+ function fnv1a(input: string): string {
301
+ let hash = 0x811c9dc5;
302
+ for (let i = 0; i < input.length; i += 1) {
303
+ hash ^= input.charCodeAt(i);
304
+ hash = Math.imul(hash, 0x01000193);
305
+ }
306
+ return (hash >>> 0).toString(16).padStart(8, '0');
307
+ }
@@ -0,0 +1,378 @@
1
+ // Gen3DAssetManifest — the durable handoff contract between gen3d and
2
+ // downstream modules (wb-3d-pipeline, game generation). See docs/adr/0002 and
3
+ // CONTEXT.md. M9: an Asset is a per-game file, keyed by its game-relative path
4
+ // (`assetPath`, e.g. assets/3d/characters/hero.glb), NOT a random UUID. The main
5
+ // GLB is the identity; same-basename sidefiles (preview PNG, external texture)
6
+ // are dependencies. Downstream modules reference assets by assetPath, never by
7
+ // provider URL.
8
+
9
+ import type { CharacterMotionOverride, MotionMappingDraft } from './playable-profile';
10
+
11
+ export type ProviderId = 'meshy' | 'hunyuan_workflow' | 'hunyuan_rest' | 'rodin' | 'visvise';
12
+
13
+ export type GenerationMode = 'text' | 'image' | 'views' | 'refine';
14
+
15
+ // Provider mode tag. `mock` marks no-quota deterministic fixtures so a manifest
16
+ // produced without a real call is never mistaken for a real generation.
17
+ export type ProviderMode = 'mock' | 'real';
18
+
19
+ export type AssetKind = 'mesh' | 'animation';
20
+
21
+ // Where the asset lands in the game's 3D asset tree. The value maps 1:1 to a
22
+ // directory under assets/3d/ (see ADR-0002 / 03-WORKSPACE-LAYOUT.md).
23
+ export type AssetSlot = 'characters' | 'meshes';
24
+
25
+ export const ASSET_SLOT_DIRS: Record<AssetSlot, string> = {
26
+ characters: 'characters',
27
+ meshes: 'meshes',
28
+ };
29
+
30
+ // Durable file roles. source_mesh/preview_image/texture come from generation;
31
+ // rigged_model/animation_clip/animated_model are appended by wb-3d-pipeline.
32
+ export type FileRole =
33
+ | 'source_mesh'
34
+ | 'rigged_model'
35
+ | 'preview_image'
36
+ | 'texture'
37
+ | 'animation_clip'
38
+ | 'animated_model';
39
+
40
+ export type FileFormat = 'glb' | 'fbx' | 'obj' | 'mtl' | 'usdz' | 'stl' | 'png' | 'jpg' | 'webp' | 'mp4';
41
+
42
+ export type SkeletonProfile = 'humanoid' | 'unknown';
43
+
44
+ // Motion type for animated_model files. Hunyuan motion_retarget v1 fixed motions
45
+ // are ints 9–16 (跨步/摔倒/跳跃/踢腿/挥击/步行/跑步/跳舞; see ADR-0003 §③). Stored
46
+ // structurally so idempotency / enumeration / downstream selection never parse
47
+ // file names. Kept as the hunyuan_v1 subset of MotionRef + a legacy on-disk
48
+ // field (older sidecars wrote a bare `motionType`; sidecarToManifest upgrades it).
49
+ export type MotionType = 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16;
50
+
51
+ // Which animation "system" produced a clip. Public beta uses Meshy; Hunyuan v1
52
+ // (8 fixed motions) stays for internal/dev; hunyuan_v2 is reserved for the
53
+ // 48-motion B-line once it unblocks (ADR-0006 §Decision 3 / PLAN §8-Q2,Q7).
54
+ export type MotionSystem = 'hunyuan_v1' | 'hunyuan_v2' | 'meshy' | 'visvise';
55
+
56
+ // Generalized, structural motion descriptor (ADR-0006, Q2 = option 1). One
57
+ // discriminated union subsumes all three systems and is designed once so adding
58
+ // hunyuan_v2 later does not touch the contract. Stored MINIMALLY as
59
+ // {system,id,label}; rich metadata (category / preview gif / rigType / isFree)
60
+ // is resolved on demand from the P2 motion catalog by (system,id), never
61
+ // persisted (PLAN §3-1). The idempotency key is `${system}:${id}`.
62
+ export type MotionRef =
63
+ | { system: 'hunyuan_v1'; id: MotionType; label: string }
64
+ | { system: 'hunyuan_v2'; id: string; label: string }
65
+ | { system: 'meshy'; id: number; label: string }
66
+ | { system: 'visvise'; id: string; label: string };
67
+
68
+ // Stable dedup / idempotency key for a motion across systems.
69
+ export function motionRefKey(ref: MotionRef): string {
70
+ return `${ref.system}:${ref.id}`;
71
+ }
72
+
73
+ // Canonical labels for the 8 Hunyuan v1 motions (跨步/摔倒/…). Kept here (not in
74
+ // the UI) so the server can build a MotionRef label and upgrade legacy sidecars.
75
+ export const HUNYUAN_V1_MOTION_LABELS: Record<MotionType, string> = {
76
+ 9: '跨步',
77
+ 10: '摔倒',
78
+ 11: '跳跃',
79
+ 12: '踢腿',
80
+ 13: '挥击',
81
+ 14: '步行',
82
+ 15: '跑步',
83
+ 16: '跳舞',
84
+ };
85
+
86
+ // Upgrade a legacy bare `motionType` (int 9–16) to a full MotionRef.
87
+ export function motionRefFromLegacy(id: MotionType): MotionRef {
88
+ return { system: 'hunyuan_v1', id, label: HUNYUAN_V1_MOTION_LABELS[id] ?? `动作 ${id}` };
89
+ }
90
+
91
+ // Reserved Meshy ids for the free walk/run clips bundled in a /rigging result.
92
+ // These are NOT real Meshy action_ids (those are positive), so reserving
93
+ // negatives lets the bundled clips dedupe + render like any other motion
94
+ // without colliding with a real action (PLAN §3-4 / §8-Q6).
95
+ export const MESHY_FREE_WALK_ID = -1;
96
+ export const MESHY_FREE_RUN_ID = -2;
97
+
98
+ export interface ManifestFile {
99
+ fileId: string;
100
+ role: FileRole;
101
+ format: FileFormat;
102
+ // Game-relative path of the on-disk file (e.g. assets/3d/characters/hero.glb).
103
+ // The main source_mesh GLB path equals the manifest's assetPath identity.
104
+ storageKey: string;
105
+ bytes: number;
106
+ sha256: string;
107
+ // Same-origin Studio URL when the blob can be streamed locally. Preview/
108
+ // download only; not assumed reachable by external providers.
109
+ localUrl: string | null;
110
+ // Rigging readiness. Only meaningful for role=rigged_model FBX inputs that
111
+ // motion_retarget consumes. Plain mesh→fbx conversions stay hasSkeleton=false.
112
+ hasSkeleton: boolean;
113
+ skeletonProfile: SkeletonProfile;
114
+ animationInputReady: boolean;
115
+ // For role=animated_model files: which motion this clip is, structural (not
116
+ // parsed from the file name) so multiple motions coexist and apply-motion
117
+ // stays idempotent per motion. `motionRef` is the generalized descriptor
118
+ // (any system); `motionType` is the legacy hunyuan_v1-only field, still
119
+ // populated for that system so older readers keep working. Undefined for
120
+ // non-animated roles.
121
+ motionRef?: MotionRef;
122
+ motionType?: MotionType;
123
+ }
124
+
125
+ // Five-dimension quality rubric kept as null placeholders. Scoring is manual/
126
+ // out-of-band background knowledge per ADR-0001; not produced at generation time.
127
+ export interface QualityScore {
128
+ geometry: number | null;
129
+ topology: number | null;
130
+ texture: number | null;
131
+ pbr: number | null;
132
+ prompt_fidelity: number | null;
133
+ total: number | null;
134
+ }
135
+
136
+ export type QualityDimSource = 'auto' | 'ai' | 'manual';
137
+
138
+ export interface QualityDim {
139
+ value: number | null;
140
+ source: QualityDimSource;
141
+ }
142
+
143
+ export interface QualityReport {
144
+ geometry: QualityDim;
145
+ topology: QualityDim;
146
+ texture: QualityDim;
147
+ pbr: QualityDim;
148
+ prompt_fidelity: QualityDim;
149
+ total: number | null;
150
+ method: 'auto' | 'auto+ai' | 'manual' | 'mixed';
151
+ rater: string;
152
+ notes: string;
153
+ scoredAt: string;
154
+ }
155
+
156
+ export function emptyQualityReport(): QualityReport {
157
+ const dim = (): QualityDim => ({ value: null, source: 'auto' });
158
+ return {
159
+ geometry: dim(),
160
+ topology: dim(),
161
+ texture: dim(),
162
+ pbr: dim(),
163
+ prompt_fidelity: dim(),
164
+ total: null,
165
+ method: 'auto',
166
+ rater: '',
167
+ notes: '',
168
+ scoredAt: '',
169
+ };
170
+ }
171
+
172
+ export function reportToScore(r: QualityReport): QualityScore {
173
+ return {
174
+ geometry: r.geometry.value,
175
+ topology: r.topology.value,
176
+ texture: r.texture.value,
177
+ pbr: r.pbr.value,
178
+ prompt_fidelity: r.prompt_fidelity.value,
179
+ total: r.total,
180
+ };
181
+ }
182
+
183
+ export interface Gen3DAssetManifest {
184
+ manifestVersion: 1;
185
+ // Canonical identity: the game-relative path of the main GLB (ADR-0002).
186
+ assetPath: string;
187
+ // The 3D asset slot this lives in (characters | meshes).
188
+ assetSlot: AssetSlot;
189
+ kind: AssetKind;
190
+ provider: ProviderId;
191
+ providerMode: ProviderMode;
192
+ mode: GenerationMode;
193
+ // Original provider job/task id, for audit. Not a stored-asset reference.
194
+ // For Meshy text after auto-refine this is the refine (result) task id.
195
+ sourceJobId: string | null;
196
+ // Meshy preview vs result task pair. Manual refine MUST use previewTaskId;
197
+ // auto-rig prefers resultTaskId (final textured mesh). Absent for non-Meshy.
198
+ meshyTaskRefs?: {
199
+ previewTaskId: string | null;
200
+ resultTaskId: string | null;
201
+ };
202
+ // VISVISE generate/rig COS URLs. auto-rig / apply-motion must send these as
203
+ // input_model_url (C1: do not re-host on ForgeaX COS first).
204
+ visviseRefs?: {
205
+ meshUrl: string | null;
206
+ rigUrl: string | null;
207
+ };
208
+ // Upstream asset paths consumed to produce this one (e.g. image→mesh).
209
+ sourceInputAssetPaths: string[];
210
+ prompt: string | null;
211
+ // Optional user-defined display name. Overrides the auto-generated caption
212
+ // (prompt first-line or mode name) everywhere the asset is shown.
213
+ userLabel?: string | null;
214
+ files: ManifestFile[];
215
+ // Readiness flags answer "what can downstream do with this asset".
216
+ readiness: {
217
+ hasSourceMesh: boolean;
218
+ rigged: boolean;
219
+ animated: boolean;
220
+ };
221
+ // Rig-chain identity once the asset is rigged (ADR-0006). apply-motion reads
222
+ // rig.rigTaskId (Meshy) and dispatches by rig.rigProvider. Undefined until rigged.
223
+ rig?: RigChain;
224
+ quality: QualityScore;
225
+ targetFaceCount?: number | null;
226
+ createdAt: string;
227
+ updatedAt: string;
228
+ }
229
+
230
+ // ─── v2 workspace-contract sidecar (03-WORKSPACE-LAYOUT.md) ──────────────────
231
+ //
232
+ // On disk every asset file gets a `<name>.glb.gen3d-meta.json` sidecar in the
233
+ // v2 contract shape (NOT engine pack `*.meta.json`). gen3d-private fields
234
+ // (provider/mode/job/cacheKey/readiness…) live under `custom`. Same-basename
235
+ // sidefiles go in `dependencies[]`.
236
+
237
+ export interface SidecarDependency {
238
+ // Path relative to the sidecar's directory (e.g. hero.png, hero.texture.png).
239
+ path: string;
240
+ // sha256:<hex>.
241
+ hash: string;
242
+ // Role of the dependency file (preview_image, texture, rigged_model, …).
243
+ kind: string;
244
+ // Rigging metadata for rigged_model / animated_model attached files, so
245
+ // sidecarToManifest can restore them instead of writing hasSkeleton:false. Only
246
+ // set by appendDerivedFiles (a verified rig step); generation never sets these.
247
+ hasSkeleton?: boolean;
248
+ skeletonProfile?: SkeletonProfile;
249
+ animationInputReady?: boolean;
250
+ // For animated_model deps: the generalized motion descriptor (any system).
251
+ motionRef?: MotionRef;
252
+ // Legacy hunyuan_v1-only field, still read for back-compat with sidecars
253
+ // written before motionRef existed (upgraded in sidecarToManifest).
254
+ motionType?: MotionType;
255
+ }
256
+
257
+ // Rig-chain identity persisted on a rigged asset (ADR-0006 §Decision 3). Meshy
258
+ // animation MUST be driven by Meshy's own rig_task_id (it does not accept an
259
+ // external FBX), so apply-motion dispatches strictly by the recorded system and
260
+ // reads rigTaskId from here. Hunyuan REST only needs the local rigged FBX, so
261
+ // rigTaskId is null there.
262
+ export interface RigChain {
263
+ rigProvider: 'meshy' | 'hunyuan_rest' | 'visvise';
264
+ // Meshy rig task id (input to /animations). null for the Hunyuan path.
265
+ rigTaskId: string | null;
266
+ // Meshy rig skeleton type (e.g. style_01); used to filter compatible actions.
267
+ rigType: string | null;
268
+ // Meshy rig task expiry (epoch ms; ~3 days). Used to detect rig_expired.
269
+ rigExpiresAt: number | null;
270
+ }
271
+
272
+ export interface PlayableDeliverySnapshot {
273
+ modelPath: string;
274
+ playablePath: string;
275
+ profileId: string;
276
+ profileVersion: number;
277
+ clipSlotIds?: string[];
278
+ slotGuidRegistry: Record<string, string>;
279
+ mappingFingerprint: string;
280
+ exportedAt: string;
281
+ }
282
+
283
+ export interface AssetSidecar {
284
+ schemaVersion: 1;
285
+ producer: {
286
+ plugin: string;
287
+ pluginVersion: string;
288
+ pipelineId?: string;
289
+ };
290
+ createdAt: string;
291
+ // sha256:<hex> of the main asset file.
292
+ contentHash: string;
293
+ size: number;
294
+ // Asset type label (e.g. gen3d-character, gen3d-mesh).
295
+ type: string;
296
+ dependencies: SidecarDependency[];
297
+ // gen3d-private namespace. Not part of the cross-plugin contract.
298
+ custom: {
299
+ provider: ProviderId;
300
+ providerMode: ProviderMode;
301
+ mode: GenerationMode;
302
+ assetSlot: AssetSlot;
303
+ sourceJobId: string | null;
304
+ prompt: string | null;
305
+ userLabel?: string | null;
306
+ sourceInputAssetPaths: string[];
307
+ faceCount?: number;
308
+ readiness: Gen3DAssetManifest['readiness'];
309
+ // The cacheKey that produced this asset, for delete→tombstone reverse lookup.
310
+ cacheKey?: string;
311
+ quality?: QualityReport;
312
+ meshyTaskRefs?: Gen3DAssetManifest['meshyTaskRefs'];
313
+ visviseRefs?: Gen3DAssetManifest['visviseRefs'];
314
+ // Rig-chain identity (ADR-0006), set by a verified rig step. apply-motion
315
+ // dispatches by custom.rig.rigProvider and reads rig.rigTaskId (Meshy).
316
+ rig?: RigChain;
317
+ // Playable-character layer (PLAN-2026-07-13 §4.1, ADR-0008). Only ever set
318
+ // on characters slot assets. Absent = this character uses the game default
319
+ // profile unmodified with no motion mapping drafted yet.
320
+ playableOverride?: CharacterMotionOverride;
321
+ motionMapping?: MotionMappingDraft;
322
+ // Last successful playable delivery snapshot (PLAN §5.6 / UX1 one-click update).
323
+ playableDelivery?: PlayableDeliverySnapshot;
324
+ };
325
+ }
326
+
327
+ export const FILE_ROLES: readonly FileRole[] = [
328
+ 'source_mesh',
329
+ 'rigged_model',
330
+ 'preview_image',
331
+ 'texture',
332
+ 'animation_clip',
333
+ 'animated_model',
334
+ ];
335
+
336
+ export function emptyQuality(): QualityScore {
337
+ return {
338
+ geometry: null,
339
+ topology: null,
340
+ texture: null,
341
+ pbr: null,
342
+ prompt_fidelity: null,
343
+ total: null,
344
+ };
345
+ }
346
+
347
+ export function computeReadiness(files: readonly ManifestFile[]): Gen3DAssetManifest['readiness'] {
348
+ return {
349
+ hasSourceMesh: files.some((file) => file.role === 'source_mesh'),
350
+ rigged: files.some((file) => file.role === 'rigged_model' && file.hasSkeleton),
351
+ animated: files.some(
352
+ (file) => file.role === 'animated_model' || file.role === 'animation_clip',
353
+ ),
354
+ };
355
+ }
356
+
357
+ // Resolve the single file a consumer wants by role (+ optional format), instead
358
+ // of parsing file names or URLs. The main mesh is selectFile(files,
359
+ // 'source_mesh', 'glb'); the preview is selectFile(files, 'preview_image').
360
+ export function selectFile(
361
+ files: readonly ManifestFile[],
362
+ role: FileRole,
363
+ format?: FileFormat,
364
+ ): ManifestFile | null {
365
+ return (
366
+ files.find((file) => file.role === role && (format ? file.format === format : true)) ?? null
367
+ );
368
+ }
369
+
370
+ // All files of a role (e.g. every animated_model clip), for enumeration. The UI
371
+ // lists existing motions from animated_model files' structural motionType.
372
+ export function selectFiles(
373
+ files: readonly ManifestFile[],
374
+ role: FileRole,
375
+ format?: FileFormat,
376
+ ): ManifestFile[] {
377
+ return files.filter((file) => file.role === role && (format ? file.format === format : true));
378
+ }