@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@call-me-sensei/toonlab",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "description": "Anime-style game toolkit for Three.js with cel shading, vegetation, rocks, water, sky, clouds, procedural textures, and portable Lab artifacts",
5
5
  "license": "MIT",
6
6
  "author": "Hyperbond Studio PTE. LTD. (Call Me Sensei)",
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/call-me-sensei/toonlab.git"
10
10
  },
11
- "homepage": "https://github.com/call-me-sensei/toonlab#readme",
11
+ "homepage": "https://toonlab.io/docs",
12
12
  "bugs": {
13
13
  "url": "https://github.com/call-me-sensei/toonlab/issues"
14
14
  },
@@ -83,14 +83,22 @@
83
83
  "src/collisionMetadata.js",
84
84
  "types",
85
85
  "mcp",
86
- "cli",
86
+ "cli/toonlab.mjs",
87
87
  "database",
88
- "agents/references",
88
+ "agents/references/anime-art-direction.md",
89
+ "agents/references/asset-sourcing-policy.md",
90
+ "agents/references/custom-gap-report.md",
91
+ "agents/references/mcp-asset-discovery.md",
92
+ "agents/references/runtime-entry-points.md",
93
+ "agents/references/style-bundles.md",
94
+ "agents/README.md",
95
+ "agents/PROMPTS.md",
96
+ "agents/claude/CLAUDE.md",
97
+ "agents/codex/AGENTS.md",
98
+ "agents/cursor/toonlab.mdc",
89
99
  "agents/skills/claude/asset-sourcing",
90
100
  "agents/skills/claude/environment",
91
101
  "agents/skills/claude/game-dev",
92
- "agents/skills/claude/karst-cliff-construction",
93
- "agents/skills/claude/outdoor-world",
94
102
  "agents/skills/claude/post-processing",
95
103
  "agents/skills/claude/rock-ground-shaders",
96
104
  "agents/skills/claude/rockgen",
@@ -103,8 +111,6 @@
103
111
  "agents/skills/codex/asset-sourcing",
104
112
  "agents/skills/codex/environment",
105
113
  "agents/skills/codex/game-dev",
106
- "agents/skills/codex/karst-cliff-construction",
107
- "agents/skills/codex/outdoor-world",
108
114
  "agents/skills/codex/post-processing",
109
115
  "agents/skills/codex/rock-ground-shaders",
110
116
  "agents/skills/codex/rockgen",
@@ -117,6 +123,7 @@
117
123
  "scripts/setup-local.mjs",
118
124
  "scripts/generate-catalog-seed.mjs",
119
125
  "compose.yaml",
126
+ "NPM-LIBRARY.md",
120
127
  "README.md",
121
128
  "LICENSE"
122
129
  ],
@@ -272,8 +279,20 @@
272
279
  "golden:compare": "node scripts/compare-golden-captures.mjs",
273
280
  "verify:rockgen": "node scripts/verify-rockgen.mjs",
274
281
  "verify:rock-generation-lab": "node scripts/verify-rock-generation-lab.mjs",
282
+ "verify:rock-c7-library": "node scripts/verify-rock-c7-surface-library.mjs",
275
283
  "verify:rock-lab-lifecycle": "node scripts/verify-rock-lab-lifecycle.mjs",
276
284
  "verify:rock-shader": "node scripts/verify-rock-shader.mjs",
285
+ "verify:rock-regions": "node scripts/verify-rock-regions.mjs",
286
+ "verify:rock-region-compiler": "node scripts/verify-rock-region-compiler.mjs",
287
+ "verify:rock-region-migrations": "node scripts/verify-rock-region-schema-migrations.mjs",
288
+ "verify:rock-regions-package": "node scripts/verify-packaged-rock-regions.mjs",
289
+ "verify:rock-regions-clean-consumer": "node scripts/verify-rock-region-clean-consumer.mjs",
290
+ "verify:rock-semantic-materials": "node scripts/verify-rock-semantic-materials.mjs",
291
+ "verify:rock-geology-v2-ontology": "node scripts/verify-rock-geology-v2-ontology.mjs",
292
+ "verify:rock-geology-v2-morphology": "node scripts/verify-rock-geology-v2-morphology.mjs",
293
+ "verify:rock-geology-v2-inspirations": "node scripts/verify-rock-geology-v2-inspirations.mjs",
294
+ "verify:rock-geology-v2-canonical-pilot": "node scripts/verify-rock-geology-v2-canonical-pilot.mjs",
295
+ "capture:rock-geology-v2-canonical-pilot": "node scripts/capture-rock-geology-v2-canonical-pilot.mjs",
277
296
  "verify:ground-shader": "node scripts/verify-ground-shader.mjs",
278
297
  "verify:pathgen": "node scripts/verify-pathgen.mjs",
279
298
  "visual:check": "node scripts/visual-check.mjs",
@@ -295,6 +314,7 @@
295
314
  "verify:localization": "node scripts/verify-localization.mjs",
296
315
  "verify:lab-asset-provenance": "node scripts/verify-lab-asset-provenance.mjs",
297
316
  "verify:lab-preview": "node scripts/verify-lab-preview.mjs",
317
+ "verify:library-preview": "node scripts/verify-library-preview-coverage.mjs",
298
318
  "verify:public-labs": "node scripts/verify-public-lab-authoring.mjs",
299
319
  "verify:vegetation-labs": "node scripts/verify-vegetation-lab-readiness.mjs",
300
320
  "verify:water-lab": "node scripts/verify-water-lab-contract.mjs",
@@ -327,7 +347,6 @@
327
347
  "verify:quality-integration": "node scripts/verify-quality-budget-integration.mjs && node scripts/verify-packaged-quality-budget-integration.mjs",
328
348
  "verify:vegetation-shader": "node scripts/verify-vegetation-shader.mjs",
329
349
  "verify:lab-styles": "node scripts/verify-lab-style-boundaries.mjs",
330
- "verify:legacy-trees": "node scripts/verify-legacy-trees.mjs",
331
350
  "verify:branch-tree": "node scripts/verify-branch-tree.mjs",
332
351
  "verify:tree-doodle": "node scripts/verify-tree-doodle.mjs",
333
352
  "verify:tree-woody": "node scripts/verify-tree-woody-engine.mjs",
@@ -345,7 +364,7 @@
345
364
  "verify:collision-package": "node scripts/verify-packaged-world-collision.mjs",
346
365
  "verify:official-catalog": "node scripts/verify-official-catalog-provider.mjs && node scripts/verify-official-catalog-runtime.mjs && node scripts/verify-official-catalog-lod.mjs && node scripts/verify-official-catalog-placement.mjs && node scripts/verify-playground-official-catalog.mjs && node scripts/verify-packaged-official-catalog.mjs",
347
366
  "verify:system-style": "node scripts/verify-system-style-policy.mjs",
348
- "verify:release": "npm run verify:system-style && npm run verify:style-contract && npm run verify:style-labels-package && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:public-labs && npm run verify:live-labs && npm run verify:rock-shader && npm run verify:rock-generation-lab && npm run verify:rock-lab-lifecycle && npm run verify:ground-shader && npm run verify:manufactured-materials && npm run verify:material-labeling:agent && npm run verify:material-labeling:mcp && npm run verify:water-lab && npm run verify:water && npm run verify:vegetation-labs && npm run verify:texgen && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:world-quality && npm run verify:scatter && npm run verify:legacy-trees && npm run verify:branch-tree && npm run verify:grass-package && npm run verify:character-package && npm run verify:locomotion-state && npm run verify:ground-stabilizer && npm run verify:water-interaction && npm run verify:walkable-character && npm run verify:playground-walkable && npm run verify:renderer-configuration && npm run verify:scene-scheduler && npm run verify:scene-surface && npm run verify:shared-shadow-receivers && npm run verify:scene-quality && npm run verify:quality-integration && npm run verify:lab-asset-provenance && npm run verify:api && npm run verify:types && npm run verify:mcp && npm run verify:skills && npm run verify:docs && npm run verify:local-database && npm run verify:package",
367
+ "verify:release": "npm run verify:system-style && npm run verify:style-contract && npm run verify:style-labels-package && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:public-labs && npm run verify:live-labs && npm run verify:rock-shader && npm run verify:rock-generation-lab && npm run verify:rock-lab-lifecycle && npm run verify:ground-shader && npm run verify:manufactured-materials && npm run verify:material-labeling:agent && npm run verify:material-labeling:mcp && npm run verify:water-lab && npm run verify:water && npm run verify:vegetation-labs && npm run verify:texgen && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:world-quality && npm run verify:scatter && npm run verify:branch-tree && npm run verify:grass-package && npm run verify:character-package && npm run verify:locomotion-state && npm run verify:ground-stabilizer && npm run verify:water-interaction && npm run verify:walkable-character && npm run verify:playground-walkable && npm run verify:renderer-configuration && npm run verify:scene-scheduler && npm run verify:scene-surface && npm run verify:shared-shadow-receivers && npm run verify:scene-quality && npm run verify:quality-integration && npm run verify:lab-asset-provenance && npm run verify:api && npm run verify:types && npm run verify:mcp && npm run verify:skills && npm run verify:docs && npm run verify:local-database && npm run verify:package",
349
368
  "verify:generation": "node scripts/verify-generation.mjs",
350
369
  "verify:post": "node scripts/verify-post.mjs",
351
370
  "verify:camera": "node scripts/verify-camera.mjs",
@@ -354,13 +373,14 @@
354
373
  "verify:soundscape": "node scripts/verify-soundscape.mjs",
355
374
  "verify:game-feel": "node scripts/verify-game-feel.mjs",
356
375
  "verify:world-quality": "node scripts/verify-world-quality.mjs",
357
- "verify:all": "npm run verify:system-style && npm run verify:product-namespace && npm run verify:generation && npm run verify:post && npm run verify:camera && npm run verify:motion && npm run verify:biome && npm run verify:soundscape && npm run verify:game-feel && npm run verify:world-quality && npm run verify:rock-shader && npm run verify:ground-shader && npm run verify:rockgen && npm run verify:rock-generation-lab && npm run verify:pathgen && npm run verify:propgen && npm run verify:buildinggen && npm run verify:villagegen && npm run verify:fauna && npm run verify:ambientfx && npm run verify:vfxgen && npm run verify:water && npm run verify:landscape && npm run verify:lighting && npm run verify:lighting-lab && npm run verify:live-labs && npm run verify:lab-preview && npm run verify:weather && npm run verify:climate && npm run verify:atmospheric-condition-lab && npm run verify:sky && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:environment-cycle && npm run verify:manufactured-materials && npm run verify:ground-field && npm run verify:grass && npm run verify:grass-package && npm run verify:character-package && npm run verify:vegetation-shader && npm run verify:lab-styles && npm run verify:scatter && npm run verify:legacy-trees && npm run verify:branch-tree && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:api && npm run verify:package && npm run verify:catalog && npm run verify:assetlib && npm run verify:mcp && npm run verify:local-generation",
376
+ "verify:all": "npm run verify:system-style && npm run verify:product-namespace && npm run verify:generation && npm run verify:post && npm run verify:camera && npm run verify:motion && npm run verify:biome && npm run verify:soundscape && npm run verify:game-feel && npm run verify:world-quality && npm run verify:rock-shader && npm run verify:ground-shader && npm run verify:rockgen && npm run verify:rock-generation-lab && npm run verify:pathgen && npm run verify:propgen && npm run verify:buildinggen && npm run verify:villagegen && npm run verify:fauna && npm run verify:ambientfx && npm run verify:vfxgen && npm run verify:water && npm run verify:landscape && npm run verify:lighting && npm run verify:lighting-lab && npm run verify:live-labs && npm run verify:lab-preview && npm run verify:weather && npm run verify:climate && npm run verify:atmospheric-condition-lab && npm run verify:sky && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:environment-cycle && npm run verify:manufactured-materials && npm run verify:ground-field && npm run verify:grass && npm run verify:grass-package && npm run verify:character-package && npm run verify:vegetation-shader && npm run verify:lab-styles && npm run verify:scatter && npm run verify:branch-tree && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:api && npm run verify:package && npm run verify:catalog && npm run verify:assetlib && npm run verify:mcp && npm run verify:local-generation",
358
377
  "verify:catalog": "node scripts/verify-catalog.mjs",
359
378
  "verify:assetlib": "node scripts/verify-assetlib.mjs",
360
379
  "verify:mcp": "node scripts/verify-mcp.mjs",
361
380
  "verify:local-database": "node scripts/verify-local-database.mjs",
381
+ "verify:creation-revisions": "node scripts/verify-creation-revisions.mjs",
362
382
  "verify:gallery-database": "node scripts/verify-official-gallery-database.mjs",
363
- "verify:local-generation": "node scripts/verify-local-generation.mjs",
383
+ "verify:local-generation": "node scripts/verify-local-generation.mjs && node scripts/verify-fal-patina.mjs",
364
384
  "thumbs:prop": "node scripts/generate-prop-thumbs.mjs",
365
385
  "thumbs:building": "node scripts/generate-building-thumbs.mjs",
366
386
  "thumbs:catalog": "node scripts/generate-catalog-thumbs.mjs",
@@ -73,6 +73,22 @@ function assertContentType(value, label) {
73
73
  }
74
74
  }
75
75
 
76
+ function assertPublicHttpsUrl(value, label, { optional = false } = {}) {
77
+ if (optional && !value) return;
78
+ let url;
79
+ try {
80
+ url = new URL(String(value ?? ''));
81
+ } catch {
82
+ throw new Error(`${label}: must be public HTTPS`);
83
+ }
84
+ if (
85
+ url.protocol !== 'https:'
86
+ || url.username
87
+ || url.password
88
+ || /[?&](?:sign|signature|token|X-Amz-Credential|X-Amz-Signature)=/i.test(url.search)
89
+ ) throw new Error(`${label}: must be public HTTPS`);
90
+ }
91
+
76
92
  function assertReview(asset, policy) {
77
93
  const review = asset.licenseReview;
78
94
  if (!review || typeof review !== 'object') throw new Error(`${asset.id}: licenseReview is required`);
@@ -110,7 +126,7 @@ function assertRockDimensions(asset) {
110
126
  function assertAsset(asset, release, baseUrl) {
111
127
  const required = [
112
128
  'id', 'source', 'kind', 'name', 'license', 'licenseUrl', 'downloadUrl',
113
- 'sha256', 'byteSize', 'contentType', 'redistributionScope', 'reviewedAt',
129
+ 'redistributionScope', 'reviewedAt',
114
130
  ];
115
131
  for (const field of required) {
116
132
  if (asset[field] == null || asset[field] === '') throw new Error(`${asset.id ?? 'asset'}: ${field} is required`);
@@ -130,13 +146,24 @@ function assertAsset(asset, release, baseUrl) {
130
146
  if (asset.attributionRequired && !String(asset.attribution ?? '').trim()) {
131
147
  throw new Error(`${asset.id}: attribution text is required`);
132
148
  }
133
- assertSha(asset.sha256, asset.id);
134
- assertBytes(asset.byteSize, asset.id);
135
- assertContentType(asset.contentType, asset.id);
149
+ const externalOnly = asset.redistributionScope === 'external-only';
150
+ if (!externalOnly) {
151
+ assertSha(asset.sha256, asset.id);
152
+ assertBytes(asset.byteSize, asset.id);
153
+ assertContentType(asset.contentType, asset.id);
154
+ } else if (asset.sha256 != null || asset.byteSize != null || asset.contentType != null) {
155
+ throw new Error(`${asset.id}: external-only assets must not claim release-owned integrity metadata`);
156
+ }
136
157
  assertRockDimensions(asset);
158
+ assertPublicHttpsUrl(asset.sourceUrl, `${asset.id}: sourceUrl`);
137
159
  const prefix = immutablePrefix(baseUrl, release, asset.id);
138
- assertUrl(asset.downloadUrl, prefix, `${asset.id}: downloadUrl`);
139
- assertUrl(asset.thumbnailUrl, prefix, `${asset.id}: thumbnailUrl`, { optional: true });
160
+ if (externalOnly) {
161
+ assertPublicHttpsUrl(asset.downloadUrl, `${asset.id}: external-only downloadUrl`);
162
+ assertPublicHttpsUrl(asset.thumbnailUrl, `${asset.id}: external-only thumbnailUrl`, { optional: true });
163
+ } else {
164
+ assertUrl(asset.downloadUrl, prefix, `${asset.id}: downloadUrl`);
165
+ assertUrl(asset.thumbnailUrl, prefix, `${asset.id}: thumbnailUrl`, { optional: true });
166
+ }
140
167
  const paths = new Set();
141
168
  const files = asset.files ?? [];
142
169
  if (!Array.isArray(files)) throw new Error(`${asset.id}: files must be an array`);
@@ -209,7 +236,7 @@ const rows = manifest.assets.map((asset) => `(
209
236
  ${sqlString(asset.sourceId)}, ${sqlString(asset.kind)}, ${sqlString(asset.name)},
210
237
  ${sqlString(asset.description)}, ${sqlString(asset.license)}, ${sqlString(asset.attribution)},
211
238
  ${sqlString(asset.sourceUrl)}, ${sqlString(asset.downloadUrl)}, ${sqlString(asset.thumbnailUrl)},
212
- ${sqlString(asset.sha256.toLowerCase())}, ${Number(asset.byteSize)}, ${sqlString(asset.contentType)},
239
+ ${sqlString(asset.sha256?.toLowerCase())}, ${asset.byteSize == null ? 'null' : Number(asset.byteSize)}, ${sqlString(asset.contentType)},
213
240
  ${sqlArray(asset.tags)}, ${sqlString(JSON.stringify(asset.metadata ?? {}))}::jsonb,
214
241
  ${sqlString(asset.licenseUrl)}, ${asset.attributionRequired === true},
215
242
  ${sqlString(asset.redistributionScope)}, ${sqlString(asset.reviewedAt)}, 'active'
@@ -7,10 +7,35 @@ export const APPROVED_CATALOG_SPDX_LICENSES = Object.freeze([
7
7
  'MIT',
8
8
  ]);
9
9
 
10
- // Custom terms are deny-by-default. Applications may maintain their own
11
- // reviewed registry, but ToonLab ships no vendor-specific custom-license
12
- // approvals or pending vendor records.
13
- export const REVIEWED_CATALOG_LICENSES = Object.freeze({});
10
+ // Custom terms are deny-by-default. Every shipped entry needs a checked-in,
11
+ // authoritative approval URL and explicit redistribution scopes.
12
+ export const REVIEWED_CATALOG_LICENSES = Object.freeze({
13
+ 'LicenseRef-PLATEAU-PDL-1.0': Object.freeze({
14
+ approvalEvidence: 'https://www.mlit.go.jp/plateau/site-policy/',
15
+ approved: true,
16
+ canonicalUrl: 'https://www.mlit.go.jp/plateau/site-policy/',
17
+ extractedFileRedistribution: true,
18
+ id: 'LicenseRef-PLATEAU-PDL-1.0',
19
+ originalArchiveRedistribution: true,
20
+ spdx: false,
21
+ status: 'approved',
22
+ }),
23
+ });
24
+
25
+ /**
26
+ * Classification for a license field that carries no value.
27
+ *
28
+ * First-party ToonLab catalog records ship `license: null` — there is no
29
+ * third-party grant to record because the asset is owned. That is a different
30
+ * situation from a third-party asset whose license is unknown, and collapsing
31
+ * the two produced a misleading failure: every first-party rock record hit
32
+ * "(missing license) is not in the reviewed-license registry", which reads as
33
+ * an unreviewed third-party license rather than an absent field.
34
+ *
35
+ * Absence is still never an approval. It is reported as its own state so a
36
+ * caller can decide, and so the error text points at the real problem.
37
+ */
38
+ export const CATALOG_LICENSE_UNSPECIFIED = 'unspecified';
14
39
 
15
40
  export function resolveCatalogLicense(id) {
16
41
  const licenseId = String(id ?? '').trim();
@@ -27,9 +52,42 @@ export function resolveCatalogLicense(id) {
27
52
  return REVIEWED_CATALOG_LICENSES[licenseId] ?? null;
28
53
  }
29
54
 
55
+ /**
56
+ * Classifies a license id without throwing, so callers can branch on the
57
+ * outcome instead of using exceptions for control flow.
58
+ *
59
+ * `not-approved` is a distinct state from `unrecognized`: the registry exists to
60
+ * hold rejected policies too (see the `!policy.approved` branch below), so
61
+ * collapsing "reviewed and refused" into "approved" would let a caller that
62
+ * branches on the state ship an asset the review explicitly rejected.
63
+ *
64
+ * @returns {{state: 'approved'|'not-approved'|'unspecified'|'unrecognized', id: string|null, policy: object|null}}
65
+ */
66
+ export function describeCatalogLicense(id) {
67
+ const licenseId = String(id ?? '').trim();
68
+ if (!licenseId) {
69
+ return { id: null, policy: null, state: CATALOG_LICENSE_UNSPECIFIED };
70
+ }
71
+ const policy = resolveCatalogLicense(licenseId);
72
+ if (!policy) return { id: licenseId, policy: null, state: 'unrecognized' };
73
+ return {
74
+ id: licenseId,
75
+ policy,
76
+ state: policy.approved ? 'approved' : 'not-approved',
77
+ };
78
+ }
79
+
30
80
  export function assertCatalogLicenseRelease({ id, redistributionScope, evidence } = {}) {
31
- const policy = resolveCatalogLicense(id);
32
- if (!policy) throw new Error(`${id || '(missing license)'} is not in the reviewed-license registry`);
81
+ const described = describeCatalogLicense(id);
82
+ if (described.state === CATALOG_LICENSE_UNSPECIFIED) {
83
+ throw new Error(
84
+ 'catalog license is unspecified: the record carries no license value. '
85
+ + 'First-party assets still need an explicit license id before release — '
86
+ + 'absence is not an approval.',
87
+ );
88
+ }
89
+ const policy = described.policy;
90
+ if (!policy) throw new Error(`${id} is not in the reviewed-license registry`);
33
91
  if (!policy.approved) throw new Error(`${id}: reviewed-license policy is ${policy.status ?? 'not approved'}`);
34
92
  if (redistributionScope !== 'external-only' && !policy.originalArchiveRedistribution) {
35
93
  throw new Error(`${id}: original-archive redistribution is not approved`);
@@ -7,6 +7,7 @@
7
7
  import * as THREE from 'three';
8
8
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
9
9
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
10
+ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
10
11
 
11
12
  import { readZipEntries } from './zip.js';
12
13
 
@@ -17,7 +18,13 @@ import { readZipEntries } from './zip.js';
17
18
  * LoadingManager URL modifier rewrites each request by suffix match, which
18
19
  * also covers URL-encoded variants.
19
20
  */
20
- export async function loadImportedModel({ url, resources = {}, dracoDecoderPath = '/draco/gltf/' }) {
21
+ export async function loadImportedModel({
22
+ url,
23
+ resources = {},
24
+ dracoDecoderPath = '/draco/gltf/',
25
+ ktx2TranscoderPath = '/basis/',
26
+ renderer = null,
27
+ }) {
21
28
  const manager = new THREE.LoadingManager();
22
29
  const byPath = Object.entries(resources);
23
30
  manager.setURLModifier((requested) => {
@@ -32,11 +39,18 @@ export async function loadImportedModel({ url, resources = {}, dracoDecoderPath
32
39
  const dracoLoader = new DRACOLoader(manager);
33
40
  dracoLoader.setDecoderPath(dracoDecoderPath);
34
41
  loader.setDRACOLoader(dracoLoader);
42
+ const ktx2Loader = renderer
43
+ ? new KTX2Loader(manager)
44
+ .setTranscoderPath(ktx2TranscoderPath)
45
+ .detectSupport(renderer)
46
+ : null;
47
+ if (ktx2Loader) loader.setKTX2Loader(ktx2Loader);
35
48
  let gltf;
36
49
  try {
37
50
  gltf = await loader.loadAsync(url);
38
51
  } finally {
39
52
  dracoLoader.dispose();
53
+ ktx2Loader?.dispose();
40
54
  }
41
55
  const object = gltf.scene ?? gltf.scenes?.[0];
42
56
  if (!object) throw new Error('loadImportedModel: glTF contained no scene.');
@@ -8,6 +8,11 @@ import {
8
8
  setObjectTextureColorSpaces,
9
9
  waitForObjectTextures,
10
10
  } from '../toon/toonMaterialAdapter.js';
11
+ import {
12
+ copyRockSemanticMaterialBinding,
13
+ prepareRockSemanticMaterialBindings,
14
+ rockSemanticMaterialTextures,
15
+ } from '../rock-shader/rockSemanticMaterialRuntime.js';
11
16
  import { OFFICIAL_CATALOG_ASSET_VERSION } from './officialCatalogProvider.js';
12
17
 
13
18
  const rendererPools = new WeakMap();
@@ -29,6 +34,9 @@ function collectModelResources(root) {
29
34
  Object.values(material).forEach((value) => {
30
35
  if (value?.isTexture) textures.add(value);
31
36
  });
37
+ for (const value of Object.values(rockSemanticMaterialTextures(material) ?? {})) {
38
+ if (value?.isTexture) textures.add(value);
39
+ }
32
40
  });
33
41
  });
34
42
  return { geometries, materials, textures };
@@ -86,7 +94,9 @@ function cloneModelRoot(sourceRoot) {
86
94
  root.traverse((object) => {
87
95
  if (!object.isMesh || !object.material) return;
88
96
  const cloneMaterial = (material) => {
89
- if (!materialClones.has(material)) materialClones.set(material, material.clone());
97
+ if (!materialClones.has(material)) {
98
+ materialClones.set(material, copyRockSemanticMaterialBinding(material, material.clone()));
99
+ }
90
100
  return materialClones.get(material);
91
101
  };
92
102
  object.material = Array.isArray(object.material)
@@ -115,9 +125,10 @@ function releaseInstanceRoot(root, sourceResources) {
115
125
  });
116
126
  }
117
127
 
118
- async function defaultPrepareTextures(root) {
128
+ async function defaultPrepareTextures(root, asset) {
119
129
  await waitForObjectTextures(root);
120
130
  setObjectTextureColorSpaces(root);
131
+ await prepareRockSemanticMaterialBindings(root, { modelUrl: asset?.modelUrl });
121
132
  }
122
133
 
123
134
  /**
@@ -57,10 +57,16 @@ export function selectCatalogLodLevel({
57
57
  distance,
58
58
  distances,
59
59
  maxLevel = Number.POSITIVE_INFINITY,
60
+ currentLevel = null,
61
+ hysteresis = 0,
60
62
  } = {}) {
61
63
  const available = [...new Set((availableLevels ?? []).map(finiteLevel).filter((v) => v !== null))]
62
64
  .sort((left, right) => left - right);
63
65
  if (available.length === 0) return null;
66
+ const margin = Number(hysteresis);
67
+ if (!Number.isFinite(margin) || margin < 0 || margin >= 0.5) {
68
+ throw new RangeError('Catalog LOD hysteresis must be a finite ratio in [0, 0.5).');
69
+ }
64
70
  const thresholds = normalizeCatalogLodDistances(distances, available.at(-1) + 1);
65
71
  const finiteDistance = Math.max(Number(distance) || 0, 0);
66
72
  let requested = 0;
@@ -69,7 +75,26 @@ export function selectCatalogLodLevel({
69
75
  }
70
76
  requested = Math.min(requested, Math.max(Number(maxLevel) || 0, 0));
71
77
  const lower = available.filter((level) => level <= requested).at(-1);
72
- return lower ?? available[0];
78
+ const selected = lower ?? available[0];
79
+
80
+ const current = finiteLevel(currentLevel);
81
+ if (current === null || !available.includes(current) || margin === 0 || selected === current) {
82
+ return selected;
83
+ }
84
+
85
+ if (selected > current) {
86
+ // Do not leave the current LOD until the first available coarser level's
87
+ // threshold plus its margin has been crossed. This also handles authored
88
+ // groups with deliberately missing intermediate levels.
89
+ const next = available.find((level) => level > current && level <= selected);
90
+ if (next !== undefined && finiteDistance < thresholds[next] * (1 + margin)) return current;
91
+ } else {
92
+ // Hold the current LOD while the camera jitters inside the lower half of
93
+ // the transition band. Crossing it permits the normal deterministic
94
+ // selector to choose one or several finer levels.
95
+ if (finiteDistance >= thresholds[current] * (1 - margin)) return current;
96
+ }
97
+ return selected;
73
98
  }
74
99
 
75
100
  /**
@@ -79,6 +104,7 @@ export function selectCatalogLodLevel({
79
104
  */
80
105
  export function createCatalogLodRuntime(root, {
81
106
  distances = [0, 45, 120],
107
+ hysteresis = 0,
82
108
  maxLevel = Number.POSITIVE_INFINITY,
83
109
  namePattern = DEFAULT_LOD_NAME_PATTERN,
84
110
  } = {}) {
@@ -87,6 +113,10 @@ export function createCatalogLodRuntime(root, {
87
113
  [...new Set(bindings.map((binding) => binding.level))].sort((a, b) => a - b),
88
114
  );
89
115
  const thresholds = normalizeCatalogLodDistances(distances, availableLevels.at(-1) + 1);
116
+ const hysteresisRatio = Number(hysteresis);
117
+ if (!Number.isFinite(hysteresisRatio) || hysteresisRatio < 0 || hysteresisRatio >= 0.5) {
118
+ throw new RangeError('Catalog LOD hysteresis must be a finite ratio in [0, 0.5).');
119
+ }
90
120
  const worldPosition = new Vector3();
91
121
  const worldScale = new Vector3();
92
122
  let currentLevel = null;
@@ -124,6 +154,8 @@ export function createCatalogLodRuntime(root, {
124
154
  availableLevels,
125
155
  distance,
126
156
  distances: thresholds,
157
+ currentLevel,
158
+ hysteresis: hysteresisRatio,
127
159
  maxLevel,
128
160
  });
129
161
  const changed = setLevel(level);
@@ -145,6 +177,7 @@ export function createCatalogLodRuntime(root, {
145
177
  get level() { return currentLevel; },
146
178
  setLevel,
147
179
  thresholds,
180
+ hysteresis: hysteresisRatio,
148
181
  update,
149
182
  });
150
183
  }
@@ -7,6 +7,7 @@ import {
7
7
  validateCollisionMetadata,
8
8
  } from '../collisionMetadata.js';
9
9
  import { applyRockShader } from '../rock-shader/rockShaderRuntime.js';
10
+ import { withoutDegenerateDetailMaps } from '../rock-shader/rockTextureIntegrity.js';
10
11
  import {
11
12
  applyStyleBundle,
12
13
  } from '../styles/styleApplication.js';
@@ -38,28 +39,37 @@ function rotation3(value) {
38
39
  return [0, Number(value) || 0, 0];
39
40
  }
40
41
 
41
- function catalogRockTextures(root) {
42
- const textures = {};
43
- root.traverse((object) => {
44
- if (!object.isMesh || textures.rockNormal) return;
45
- const source = (Array.isArray(object.material) ? object.material : [object.material])
46
- .find((material) => material?.normalMap);
47
- if (source) textures.rockNormal = source.normalMap;
48
- });
49
- return textures;
42
+ /**
43
+ * Validates caller-supplied style textures without reclassifying the asset's
44
+ * own UV-space maps as triplanar detail maps. applyRockShader reads the full
45
+ * original material directly (albedo, metallic/roughness, normal, occlusion,
46
+ * specular, and emissive); harvesting its normal into `rockNormal` would apply
47
+ * that same map twice and with the wrong coordinate system.
48
+ */
49
+ function catalogRockTextures(overrides = {}) {
50
+ const usable = withoutDegenerateDetailMaps(overrides);
51
+ return {
52
+ rejected: usable.rejected,
53
+ textures: usable.textures,
54
+ };
50
55
  }
51
56
 
52
- function styleAdapterFor(asset, root) {
57
+ function styleAdapterFor(asset, root, { textures: overrides, variation }) {
53
58
  if (asset.domain !== 'natural.rock') return null;
54
- const textures = catalogRockTextures(root);
59
+ const resolved = catalogRockTextures(overrides);
60
+ const report = { rejectedTextures: resolved.rejected };
55
61
  return Object.freeze({
56
62
  apply(subject, settings) {
57
- return applyRockShader(subject, settings, {
63
+ const result = applyRockShader(subject, settings, {
58
64
  name: `ToonLab · ${asset.label}`,
59
- textures,
65
+ textures: resolved.textures,
66
+ variation,
60
67
  });
68
+ report.rejectedTextures = [...resolved.rejected, ...(result.rejectedTextures ?? [])];
69
+ return result;
61
70
  },
62
71
  id: 'toonlab-official-catalog-rock',
72
+ report,
63
73
  });
64
74
  }
65
75
 
@@ -113,6 +123,8 @@ export async function loadOfficialCatalogAsset({
113
123
  scale = [1, 1, 1],
114
124
  styleBundle,
115
125
  targetId = null,
126
+ textures = {},
127
+ variation = 0,
116
128
  } = {}) {
117
129
  if (!assetRuntime?.acquireAsset) {
118
130
  throw new TypeError('loadOfficialCatalogAsset requires an official catalog asset runtime.');
@@ -136,6 +148,7 @@ export async function loadOfficialCatalogAsset({
136
148
  container.updateWorldMatrix(true, true);
137
149
 
138
150
  let metadata;
151
+ let adapter = null;
139
152
  let style = null;
140
153
  let lod = null;
141
154
  let collisionRegistration = null;
@@ -147,7 +160,7 @@ export async function loadOfficialCatalogAsset({
147
160
  collision: metadata,
148
161
  targetId: id,
149
162
  }));
150
- const adapter = styleAdapterFor(asset, root);
163
+ adapter = styleAdapterFor(asset, root, { textures, variation });
151
164
  style = await applyStyleBundle(styleBundle, {
152
165
  targets: [createStyleTarget(id, asset.domain, root, { adapter })],
153
166
  });
@@ -209,6 +222,8 @@ export async function loadOfficialCatalogAsset({
209
222
  normalization,
210
223
  object: root,
211
224
  quality: qualityProfile,
225
+ /** Detail maps the artifact shipped that were too small to be usable. */
226
+ get rejectedTextures() { return adapter?.report?.rejectedTextures ?? []; },
212
227
  async release() {
213
228
  if (released) return false;
214
229
  released = true;
@@ -838,6 +838,7 @@ async function loadPackagedLocomotionClipsForTarget(targetMesh, rig, {
838
838
  retargetMode = 'world',
839
839
  rootMotion = false,
840
840
  sourceUrl = TOONLAB_MANNEQUIN_ASSET_URL,
841
+ trackNameStyle = 'skeleton',
841
842
  } = {}) {
842
843
  const sourceAsset = await loadModelAsset(sourceUrl);
843
844
  let sourceMesh = null;
@@ -942,6 +943,7 @@ async function loadPackagedLocomotionClipsForTarget(targetMesh, rig, {
942
943
  mode: retargetMode,
943
944
  rootMotion,
944
945
  sourceRig,
946
+ trackNameStyle,
945
947
  },
946
948
  );
947
949
  const clip = spec.filter(retargeted);
@@ -14,7 +14,12 @@
14
14
 
15
15
  // Mixamo bone per VRM humanoid role. Also the source-side chain map used for
16
16
  // rest-pose direction corrections lives on these names (MIXAMO_CHAIN_CHILD).
17
- const MIXAMO_BONE_BY_ROLE = Object.freeze({
17
+ /**
18
+ * The humanoid contract itself: role -> canonical Mixamo bone name. Exported so
19
+ * a host can audit a rig against it (which roles resolved, which are missing)
20
+ * rather than inferring the answer from a bone count.
21
+ */
22
+ export const MIXAMO_BONE_BY_ROLE = Object.freeze({
18
23
  hips: 'mixamorigHips',
19
24
  spine: 'mixamorigSpine',
20
25
  chest: 'mixamorigSpine1',