@call-me-sensei/toonlab 0.4.19 → 0.4.21

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 (123) hide show
  1. package/NPM-LIBRARY.md +230 -0
  2. package/README.md +78 -36
  3. package/agents/PROMPTS.md +91 -0
  4. package/agents/README.md +9 -14
  5. package/agents/claude/CLAUDE.md +8 -0
  6. package/agents/codex/AGENTS.md +161 -0
  7. package/agents/cursor/toonlab.mdc +37 -0
  8. package/agents/references/mcp-asset-discovery.md +12 -9
  9. package/agents/references/runtime-entry-points.md +5 -4
  10. package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
  11. package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
  12. package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
  13. package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
  14. package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
  15. package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
  16. package/cli/toonlab.mjs +3 -2
  17. package/database/fal-patina.mjs +131 -0
  18. package/database/generation-service.mjs +108 -7
  19. package/database/migrations/0007_creation_revisions.sql +80 -0
  20. package/database/migrations/0008_creation_revision_hardening.sql +98 -0
  21. package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
  22. package/database/migrations/0010_external_catalog_assets.sql +28 -0
  23. package/database/providers.mjs +195 -16
  24. package/database/repository.mjs +518 -70
  25. package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
  26. package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
  27. package/mcp/public-catalog.mjs +4 -52
  28. package/mcp/server.mjs +175 -12
  29. package/mcp/vite-plugin.mjs +78 -2
  30. package/package.json +32 -12
  31. package/scripts/generate-catalog-seed.mjs +34 -7
  32. package/src/asset-policy/catalogLicenses.js +64 -6
  33. package/src/assetlib/loadImported.js +15 -1
  34. package/src/catalog/officialCatalogAssetRuntime.js +13 -2
  35. package/src/catalog/officialCatalogLod.js +34 -1
  36. package/src/catalog/officialCatalogPlacement.js +29 -14
  37. package/src/character/animationRetarget.js +2 -0
  38. package/src/character/characterRig.js +6 -1
  39. package/src/character/characterRuntime.js +175 -7
  40. package/src/environment/environmentMaterialAdapter.js +30 -0
  41. package/src/environment/environmentPresets.js +90 -0
  42. package/src/environment/environmentSunShadowPass.js +42 -0
  43. package/src/environment/toonLabSurfaceLighting.js +108 -10
  44. package/src/environment/urbanPropMaterial.js +104 -2
  45. package/src/lighting/lightingSystem.js +65 -10
  46. package/src/renderer/index.js +7 -0
  47. package/src/renderer/styleComparison.js +985 -0
  48. package/src/rock-shader/index.js +5 -0
  49. package/src/rock-shader/rockGeometryDetail.js +701 -0
  50. package/src/rock-shader/rockMaterial.js +782 -39
  51. package/src/rock-shader/rockRegionRuntime.js +185 -0
  52. package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
  53. package/src/rock-shader/rockShaderRuntime.js +354 -67
  54. package/src/rock-shader/rockShaderSettings.js +246 -16
  55. package/src/rock-shader/rockTangentIntegrity.js +136 -0
  56. package/src/rock-shader/rockTextureIntegrity.js +115 -0
  57. package/src/rockgen/index.js +1 -0
  58. package/src/rockgen/rockDocument.js +27 -3
  59. package/src/rockgen/surface/c7GeologySurface.js +372 -0
  60. package/src/shaders-tsl/water.js +8 -0
  61. package/src/sky/cloudShadow.js +25 -1
  62. package/src/sky/skySystem.js +9 -0
  63. package/src/styles/index.js +23 -0
  64. package/src/styles/neutralStylePresets.js +712 -0
  65. package/src/styles/sceneStyleRuntime.js +51 -4
  66. package/src/styles/styleAdapters.js +13 -2
  67. package/src/styles/styleBundle.js +1 -1
  68. package/src/texgen/evaluateTexture.js +30 -3
  69. package/src/texgen/textureGenerators.js +63 -31
  70. package/src/texgen/textureSettings.js +25 -8
  71. package/src/toon/toonSettings.js +96 -2
  72. package/src/vegetation/branchTree.js +327 -42
  73. package/src/vegetation/index.js +24 -2
  74. package/src/vegetation/scatter.js +372 -1
  75. package/src/vegetation/stylizedTree.js +312 -126
  76. package/src/vegetation/stylizedTreeFoliage.js +229 -7
  77. package/src/vegetation/treeSurfaceTextures.js +328 -0
  78. package/src/version.js +1 -1
  79. package/src/water/waterSettings.js +15 -7
  80. package/types/asset-policy/catalogLicenses.d.ts +2 -0
  81. package/types/asset-policy/index.d.ts +2 -0
  82. package/types/assetlib/loadImported.d.ts +3 -1
  83. package/types/catalog/officialCatalogLod.d.ts +63 -5
  84. package/types/character/characterRig.d.ts +59 -0
  85. package/types/character/characterRuntime.d.ts +1 -0
  86. package/types/environment/environmentMaterialAdapter.d.ts +3 -1
  87. package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
  88. package/types/index.d.ts +89 -7
  89. package/types/lighting/lightingSystem.d.ts +7 -0
  90. package/types/react/index.d.ts +20 -0
  91. package/types/renderer/index.d.ts +1 -0
  92. package/types/renderer/styleComparison.d.ts +409 -0
  93. package/types/rock-shader/index.d.ts +5 -0
  94. package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
  95. package/types/rock-shader/rockMaterial.d.ts +3 -0
  96. package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
  97. package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
  98. package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
  99. package/types/rock-shader/rockShaderSettings.d.ts +19 -5
  100. package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
  101. package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
  102. package/types/rockgen/index.d.ts +1 -0
  103. package/types/rockgen/rockDocument.d.ts +29 -1
  104. package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
  105. package/types/sky/cloudShadow.d.ts +1 -0
  106. package/types/styles/index.d.ts +1 -0
  107. package/types/styles/neutralStylePresets.d.ts +944 -0
  108. package/types/styles/sceneStyleRuntime.d.ts +20 -0
  109. package/types/vegetation/branchTree.d.ts +4 -0
  110. package/types/vegetation/index.d.ts +9 -7
  111. package/types/vegetation/scatter.d.ts +1 -0
  112. package/types/vegetation/stylizedTree.d.ts +1 -2
  113. package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
  114. package/types/version.d.ts +1 -1
  115. package/agents/references/geology-playbook.md +0 -118
  116. package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
  117. package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
  118. package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
  119. package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
  120. package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
  121. package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
  122. package/src/vegetation/legacyTreePresets.js +0 -120
  123. package/types/vegetation/legacyTreePresets.d.ts +0 -22
@@ -1,3 +1,9 @@
1
+ import {
2
+ createPatinaInput,
3
+ normalizePatinaResult,
4
+ PATINA_MATERIAL_MODEL,
5
+ } from './fal-patina.mjs';
6
+
1
7
  const IMAGE_KINDS = new Set(['image', 'texture_image', 'concept_image']);
2
8
  const TRIPO_API_BASE = 'https://api.tripo3d.ai/v2/openapi';
3
9
  const TRIPO_MODEL_VERSION = 'v2.5-20250123';
@@ -52,6 +58,7 @@ const ARK_IMAGE_SIZES = {
52
58
  function providerKey(provider) {
53
59
  const names = {
54
60
  ark: ['ARK_API_KEY', 'ARK_API_KEYS'],
61
+ fal: ['FAL_KEY', 'FAL_KEYS'],
55
62
  gemini: ['GEMINI_API_KEY', 'GEMINI_API_KEYS'],
56
63
  openai: ['OPENAI_API_KEY', 'OPENAI_API_KEYS'],
57
64
  tripo: ['TRIPO_API_KEY', 'TRIPO_API_KEYS'],
@@ -64,12 +71,63 @@ function providerKey(provider) {
64
71
  throw new Error(`${provider} is not configured in the server environment`);
65
72
  }
66
73
 
67
- async function checkedJson(response, provider) {
74
+ async function runFalPatina(request, key) {
75
+ if (request.kind !== 'material') throw new Error(`Fal cannot serve generation kind ${request.kind}.`);
76
+ const input = createPatinaInput(request);
77
+ const response = await fetch(`https://fal.run/${PATINA_MATERIAL_MODEL}`, {
78
+ body: JSON.stringify(input),
79
+ headers: {
80
+ authorization: `Key ${key}`,
81
+ 'content-type': 'application/json',
82
+ },
83
+ method: 'POST',
84
+ signal: AbortSignal.timeout(15 * 60_000),
85
+ });
86
+ const data = await checkedJson(response, 'Fal Patina');
87
+ return {
88
+ material: normalizePatinaResult(data, input),
89
+ provider: 'fal',
90
+ };
91
+ }
92
+
93
+ function boundedProviderText(value, maximumLength = 500) {
94
+ if (value === undefined || value === null) return null;
95
+ return String(value).replace(/[\u0000-\u001f\u007f]/g, ' ').trim().slice(0, maximumLength) || null;
96
+ }
97
+
98
+ function sanitizedTraceId(value) {
99
+ const traceId = boundedProviderText(value, 160);
100
+ if (!traceId) return null;
101
+ return /^[A-Za-z0-9._:/-]+$/.test(traceId) ? traceId : '[redacted-invalid-trace-id]';
102
+ }
103
+
104
+ function providerErrorMetadata(response, body, context = {}) {
105
+ const providerCode = body?.error?.code ?? body?.code ?? null;
106
+ return {
107
+ providerCode: typeof providerCode === 'number' || typeof providerCode === 'string'
108
+ ? providerCode
109
+ : null,
110
+ providerMessage: boundedProviderText(body?.error?.message ?? body?.message),
111
+ providerRequestPhase: boundedProviderText(context.requestPhase, 64),
112
+ providerResponseReceived: true,
113
+ providerStatus: response.status,
114
+ providerSuggestion: boundedProviderText(body?.error?.suggestion ?? body?.suggestion),
115
+ retryAfter: boundedProviderText(response.headers.get('retry-after'), 160),
116
+ traceId: sanitizedTraceId(response.headers.get('x-tripo-trace-id')),
117
+ };
118
+ }
119
+
120
+ function createProviderResponseError(provider, response, body, context = {}) {
121
+ const metadata = providerErrorMetadata(response, body, context);
122
+ return Object.assign(new Error(`${provider} request failed (${response.status}): ${
123
+ metadata.providerMessage ?? 'unknown provider error'
124
+ }`), metadata);
125
+ }
126
+
127
+ export async function checkedJson(response, provider, context = {}) {
68
128
  const body = await response.json().catch(() => null);
69
129
  if (!response.ok) {
70
- throw Object.assign(new Error(`${provider} request failed (${response.status}): ${
71
- body?.error?.message ?? body?.message ?? 'unknown provider error'
72
- }`), { providerStatus: response.status });
130
+ throw createProviderResponseError(provider, response, body, context);
73
131
  }
74
132
  return body;
75
133
  }
@@ -258,9 +316,16 @@ async function tripoJson(path, key, init = {}) {
258
316
  },
259
317
  signal: init.signal ?? AbortSignal.timeout(2 * 60_000),
260
318
  });
261
- const envelope = await checkedJson(response, 'Tripo');
319
+ const requestPhase = path === '/task' && init.method === 'POST'
320
+ ? 'task-create'
321
+ : path === '/upload' && init.method === 'POST'
322
+ ? 'image-upload'
323
+ : path.startsWith('/task/') && init.method === 'GET'
324
+ ? 'task-poll'
325
+ : 'tripo-request';
326
+ const envelope = await checkedJson(response, 'Tripo', { requestPhase });
262
327
  if (envelope?.code !== 0 || !envelope?.data) {
263
- throw new Error(`Tripo request failed: ${envelope?.message ?? 'missing response data'}`);
328
+ throw createProviderResponseError('Tripo', response, envelope, { requestPhase });
264
329
  }
265
330
  return envelope.data;
266
331
  }
@@ -280,15 +345,88 @@ async function uploadTripoImage(reference, key) {
280
345
  };
281
346
  }
282
347
 
348
+ function tripoModelVersion(request) {
349
+ const modelVersion = request.modelVersion ?? TRIPO_MODEL_VERSION;
350
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(modelVersion)) {
351
+ throw new RangeError('Tripo modelVersion must be a provider snapshot identifier.');
352
+ }
353
+ return modelVersion;
354
+ }
355
+
356
+ function tripoGenerationOptions(request, modelVersion) {
357
+ const options = {};
358
+ if (request.faceLimit !== undefined) {
359
+ const geometryQuality = request.geometryQuality ?? 'standard';
360
+ const maximumByModel = {
361
+ 'P1-20260311': 20_000,
362
+ 'v2.5-20250123': 500_000,
363
+ 'v3.0-20250812': geometryQuality === 'detailed' ? 2_000_000 : 1_500_000,
364
+ 'v3.1-20260211': geometryQuality === 'detailed' ? 2_000_000 : 1_500_000,
365
+ };
366
+ const maximum = maximumByModel[modelVersion];
367
+ if (maximum === undefined) {
368
+ throw new RangeError(`Explicit Tripo faceLimit is not verified for model ${modelVersion}.`);
369
+ }
370
+ if (!Number.isInteger(request.faceLimit) || request.faceLimit < 48 || request.faceLimit > maximum) {
371
+ throw new RangeError(`Tripo ${modelVersion} faceLimit must be an integer from 48 through ${maximum}.`);
372
+ }
373
+ options.face_limit = request.faceLimit;
374
+ }
375
+ if (request.modelSeed !== undefined) {
376
+ if (!Number.isSafeInteger(request.modelSeed)) throw new RangeError('Tripo modelSeed must be a safe integer.');
377
+ options.model_seed = request.modelSeed;
378
+ }
379
+ if (request.textureSeed !== undefined) {
380
+ if (!Number.isSafeInteger(request.textureSeed)) throw new RangeError('Tripo textureSeed must be a safe integer.');
381
+ options.texture_seed = request.textureSeed;
382
+ }
383
+ for (const [requestKey, payloadKey] of [
384
+ ['texture', 'texture'],
385
+ ['pbr', 'pbr'],
386
+ ['exportUv', 'export_uv'],
387
+ ]) {
388
+ if (request[requestKey] !== undefined) {
389
+ if (typeof request[requestKey] !== 'boolean') {
390
+ throw new TypeError(`Tripo ${requestKey} must be boolean.`);
391
+ }
392
+ options[payloadKey] = request[requestKey];
393
+ }
394
+ }
395
+ if (request.textureQuality !== undefined) {
396
+ if (!['standard', 'detailed', 'extreme'].includes(request.textureQuality)) {
397
+ throw new RangeError('Tripo textureQuality must be standard, detailed, or extreme.');
398
+ }
399
+ options.texture_quality = request.textureQuality;
400
+ }
401
+ if (request.textureAlignment !== undefined) {
402
+ if (!['original_image', 'geometry'].includes(request.textureAlignment)) {
403
+ throw new RangeError('Tripo textureAlignment must be original_image or geometry.');
404
+ }
405
+ options.texture_alignment = request.textureAlignment;
406
+ }
407
+ if (request.geometryQuality !== undefined) {
408
+ if (!['standard', 'detailed'].includes(request.geometryQuality)) {
409
+ throw new RangeError('Tripo geometryQuality must be standard or detailed.');
410
+ }
411
+ if (!['v3.0-20250812', 'v3.1-20260211'].includes(modelVersion)) {
412
+ throw new RangeError('Tripo geometryQuality is supported only for v3.0-20250812 and v3.1-20260211.');
413
+ }
414
+ options.geometry_quality = request.geometryQuality;
415
+ }
416
+ return options;
417
+ }
418
+
283
419
  async function runTripo(request, key) {
284
420
  let payload;
421
+ const modelVersion = tripoModelVersion(request);
285
422
  if (request.kind === 'image_to_model') {
286
423
  const reference = request.referenceImages?.[0];
287
424
  if (!reference) throw new Error('Image-to-3D requires one reference image');
288
425
  payload = {
289
426
  type: 'image_to_model',
290
- model_version: TRIPO_MODEL_VERSION,
427
+ model_version: modelVersion,
291
428
  file: await uploadTripoImage(reference, key),
429
+ ...tripoGenerationOptions(request, modelVersion),
292
430
  };
293
431
  } else if (request.kind === 'multiview_to_model') {
294
432
  const views = request.viewImages ?? [];
@@ -297,10 +435,11 @@ async function runTripo(request, key) {
297
435
  }
298
436
  payload = {
299
437
  type: 'multiview_to_model',
300
- model_version: TRIPO_MODEL_VERSION,
438
+ model_version: modelVersion,
301
439
  files: await Promise.all(
302
440
  views.slice(0, 4).map((view) => view ? uploadTripoImage(view, key) : {}),
303
441
  ),
442
+ ...tripoGenerationOptions(request, modelVersion),
304
443
  };
305
444
  } else if (request.kind === 'model_segment') {
306
445
  if (!request.originalTaskId) throw new Error('Model segmentation requires the original Tripo task id');
@@ -312,8 +451,9 @@ async function runTripo(request, key) {
312
451
  const styleSuffix = TRIPO_STYLES[request.style] ?? TRIPO_STYLES.stylized;
313
452
  payload = {
314
453
  type: 'text_to_model',
315
- model_version: TRIPO_MODEL_VERSION,
454
+ model_version: modelVersion,
316
455
  prompt: `${request.prompt ?? request.user ?? ''}${styleSuffix}`,
456
+ ...tripoGenerationOptions(request, modelVersion),
317
457
  };
318
458
  }
319
459
  const data = await tripoJson('/task', key, {
@@ -322,7 +462,12 @@ async function runTripo(request, key) {
322
462
  method: 'POST',
323
463
  });
324
464
  if (!data.task_id) throw new Error('Tripo task creation returned no task id');
325
- return { pending: true, provider: 'tripo', taskId: data.task_id };
465
+ return {
466
+ modelVersion: request.kind === 'model_segment' ? null : modelVersion,
467
+ pending: true,
468
+ provider: 'tripo',
469
+ taskId: data.task_id,
470
+ };
326
471
  }
327
472
 
328
473
  function meshyImageDataUri(reference) {
@@ -345,17 +490,49 @@ async function meshyJson(path, key, init = {}) {
345
490
  return checkedJson(response, 'Meshy');
346
491
  }
347
492
 
493
+ function meshyGenerationOptions(request) {
494
+ const textureResolution = request.textureResolution ?? '2k';
495
+ if (!['2k', '4k', '8k'].includes(textureResolution)) {
496
+ throw new RangeError('Meshy textureResolution must be 2k, 4k, or 8k.');
497
+ }
498
+ const options = {
499
+ alpha_thumbnail: request.alphaThumbnail ?? true,
500
+ enable_pbr: request.pbr ?? true,
501
+ image_enhancement: request.imageEnhancement ?? true,
502
+ remove_lighting: request.removeLighting ?? true,
503
+ should_remesh: request.shouldRemesh ?? false,
504
+ should_texture: request.texture ?? true,
505
+ target_formats: ['glb'],
506
+ texture_resolution: textureResolution,
507
+ };
508
+ for (const [requestKey, payloadKey] of [
509
+ ['alphaThumbnail', 'alpha_thumbnail'],
510
+ ['imageEnhancement', 'image_enhancement'],
511
+ ['pbr', 'enable_pbr'],
512
+ ['removeLighting', 'remove_lighting'],
513
+ ['shouldRemesh', 'should_remesh'],
514
+ ['texture', 'should_texture'],
515
+ ]) {
516
+ if (request[requestKey] !== undefined && typeof request[requestKey] !== 'boolean') {
517
+ throw new TypeError(`Meshy ${requestKey} must be boolean.`);
518
+ }
519
+ if (request[requestKey] !== undefined) options[payloadKey] = request[requestKey];
520
+ }
521
+ if (request.texturePrompt !== undefined) {
522
+ if (typeof request.texturePrompt !== 'string' || request.texturePrompt.length > 600) {
523
+ throw new RangeError('Meshy texturePrompt must be a string of at most 600 characters.');
524
+ }
525
+ if (request.texturePrompt.trim()) options.texture_prompt = request.texturePrompt.trim();
526
+ }
527
+ return options;
528
+ }
529
+
348
530
  async function runMeshy(request, key) {
349
531
  let path;
350
532
  let payload;
351
533
  const shared = {
352
534
  ai_model: MESHY_MODEL_VERSION,
353
- alpha_thumbnail: true,
354
- enable_pbr: true,
355
- should_remesh: false,
356
- should_texture: true,
357
- target_formats: ['glb'],
358
- texture_resolution: '2k',
535
+ ...meshyGenerationOptions(request),
359
536
  };
360
537
  if (request.kind === 'image_to_model') {
361
538
  path = '/v1/image-to-3d';
@@ -382,6 +559,7 @@ async function runMeshy(request, key) {
382
559
  });
383
560
  if (!data?.result) throw new Error('Meshy task creation returned no task id');
384
561
  return {
562
+ modelVersion: MESHY_MODEL_VERSION,
385
563
  pending: true,
386
564
  provider: 'meshy',
387
565
  taskId: data.result,
@@ -390,6 +568,7 @@ async function runMeshy(request, key) {
390
568
 
391
569
  export async function runProvider(provider, request) {
392
570
  const key = providerKey(provider);
571
+ if (provider === 'fal') return runFalPatina(request, key);
393
572
  if (provider === 'gemini') return runGemini(request, key);
394
573
  if (provider === 'openai' || provider === 'ark') {
395
574
  return runOpenAiOrArk(provider, request, key);