@call-me-sensei/toonlab 0.4.21 → 0.4.23

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 (129) hide show
  1. package/NPM-LIBRARY.md +87 -27
  2. package/README.md +11 -10
  3. package/agents/PROMPTS.md +17 -3
  4. package/agents/README.md +13 -3
  5. package/agents/claude/CLAUDE.md +3 -0
  6. package/agents/codex/AGENTS.md +112 -159
  7. package/agents/cursor/toonlab.mdc +7 -3
  8. package/agents/examples/game-foundation.mjs +104 -0
  9. package/agents/references/game-lifecycle.md +57 -0
  10. package/agents/references/runtime-entry-points.md +26 -0
  11. package/agents/references/style-bundles.md +4 -1
  12. package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
  13. package/agents/skills/claude/environment/SKILL.md +4 -0
  14. package/agents/skills/claude/game-dev/SKILL.md +63 -87
  15. package/agents/skills/claude/lighting/SKILL.md +36 -0
  16. package/agents/skills/claude/post-processing/SKILL.md +8 -3
  17. package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
  18. package/agents/skills/claude/rockgen/SKILL.md +52 -21
  19. package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
  20. package/agents/skills/claude/style-presets/SKILL.md +11 -8
  21. package/agents/skills/claude/toon-shading/SKILL.md +4 -0
  22. package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
  23. package/agents/skills/claude/visual-verification/SKILL.md +45 -119
  24. package/agents/skills/claude/water/SKILL.md +6 -2
  25. package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
  26. package/agents/skills/codex/environment/SKILL.md +4 -0
  27. package/agents/skills/codex/game-dev/SKILL.md +63 -87
  28. package/agents/skills/codex/lighting/SKILL.md +36 -0
  29. package/agents/skills/codex/post-processing/SKILL.md +8 -3
  30. package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
  31. package/agents/skills/codex/rockgen/SKILL.md +52 -21
  32. package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
  33. package/agents/skills/codex/style-presets/SKILL.md +11 -8
  34. package/agents/skills/codex/toon-shading/SKILL.md +4 -0
  35. package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
  36. package/agents/skills/codex/visual-verification/SKILL.md +45 -119
  37. package/agents/skills/codex/water/SKILL.md +6 -2
  38. package/database/apply-one-catalog-seed.mjs +63 -0
  39. package/database/providers.mjs +1 -0
  40. package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
  41. package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
  42. package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
  43. package/mcp/lab-management.mjs +11 -8
  44. package/mcp/public-catalog.mjs +0 -16
  45. package/mcp/tree-lab-contract.mjs +2381 -0
  46. package/mcp/vite-plugin.mjs +5 -1
  47. package/package.json +29 -18
  48. package/src/catalog/officialCatalog.js +1 -0
  49. package/src/catalog/officialCatalogAssetRuntime.js +98 -1
  50. package/src/catalog/officialCatalogLod.js +160 -16
  51. package/src/catalog/officialCatalogPlacement.js +52 -12
  52. package/src/catalog/officialCatalogProvider.js +79 -8
  53. package/src/catalog/officialCatalogRockPackage.js +172 -0
  54. package/src/cloud/cloudReprojection.js +40 -7
  55. package/src/cloud/cloudVolume.js +44 -7
  56. package/src/cloud/noise/baseShapeVolume.js +8 -6
  57. package/src/cloud/noise/cirrusMap.js +2 -1
  58. package/src/cloud/noise/curlNoise.js +2 -1
  59. package/src/cloud/noise/erosionVolume.js +2 -1
  60. package/src/cloud/noise/textureCache.js +77 -0
  61. package/src/cloud/noise/weatherMap.js +2 -1
  62. package/src/core/sha256.js +83 -0
  63. package/src/ground-shader/groundShaderMaterial.js +12 -3
  64. package/src/rock-shader/rockMaterial.js +205 -28
  65. package/src/rock-shader/rockShaderRuntime.js +17 -1
  66. package/src/rock-shader/rockShaderSettings.js +16 -0
  67. package/src/rockgen/index.js +9 -0
  68. package/src/rockgen/lod/index.js +1 -0
  69. package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
  70. package/src/rockgen/rockDocument.js +899 -35
  71. package/src/rockgen/surface/c7GeologySurface.js +39 -9
  72. package/src/rockgen/surface/naturalRockSurface.js +1948 -0
  73. package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
  74. package/src/shaders-tsl/chunks/water-foam.js +6 -6
  75. package/src/shaders-tsl/chunks/water-lighting.js +28 -5
  76. package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
  77. package/src/shaders-tsl/chunks/water-waves.js +10 -5
  78. package/src/shaders-tsl/flower.js +7 -4
  79. package/src/shaders-tsl/grass.js +4 -3
  80. package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
  81. package/src/shaders-tsl/water.js +209 -41
  82. package/src/sky/skySystem.js +30 -7
  83. package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
  84. package/src/toon/toonSettings.js +74 -0
  85. package/src/vegetation/stylizedFlower.js +6 -0
  86. package/src/version.js +1 -1
  87. package/src/water/waterDetailSpectrum.js +91 -0
  88. package/src/water/waterDynamics.js +422 -0
  89. package/src/water/waterFoamParticles.js +196 -0
  90. package/src/water/waterFoamTexture.js +57 -0
  91. package/src/water/waterHydrodynamics.js +209 -0
  92. package/src/water/waterMaterial.js +1 -0
  93. package/src/water/waterRenderExtension.js +107 -0
  94. package/src/water/waterScenePasses.js +14 -6
  95. package/src/water/waterSettings.js +53 -26
  96. package/src/water/waterShoreMaterial.js +38 -14
  97. package/src/water/waterSpectralOcean.js +137 -0
  98. package/src/water/waterStageSettings.js +19 -1
  99. package/src/water/waterSurface.js +62 -18
  100. package/src/water/waterUnderwaterAtmosphere.js +30 -3
  101. package/types/catalog/officialCatalog.d.ts +1 -0
  102. package/types/catalog/officialCatalogPlacement.d.ts +4 -0
  103. package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
  104. package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
  105. package/types/cloud/noise/textureCache.d.ts +12 -0
  106. package/types/core/sha256.d.ts +2 -0
  107. package/types/index.d.ts +30 -0
  108. package/types/rock-shader/rockMaterial.d.ts +3 -0
  109. package/types/rock-shader/rockShaderSettings.d.ts +2 -0
  110. package/types/rockgen/index.d.ts +1 -0
  111. package/types/rockgen/lod/index.d.ts +1 -0
  112. package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
  113. package/types/rockgen/rockDocument.d.ts +654 -71
  114. package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
  115. package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
  116. package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
  117. package/types/shaders-tsl/water.d.ts +2 -1
  118. package/types/version.d.ts +1 -1
  119. package/types/water/waterDetailSpectrum.d.ts +13 -0
  120. package/types/water/waterDynamics.d.ts +89 -0
  121. package/types/water/waterFoamParticles.d.ts +32 -0
  122. package/types/water/waterFoamTexture.d.ts +1 -0
  123. package/types/water/waterHydrodynamics.d.ts +49 -0
  124. package/types/water/waterRenderExtension.d.ts +16 -0
  125. package/types/water/waterScenePasses.d.ts +1 -1
  126. package/types/water/waterSettings.d.ts +1 -0
  127. package/types/water/waterSpectralOcean.d.ts +17 -0
  128. package/types/water/waterStageSettings.d.ts +1 -1
  129. package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
@@ -154,7 +154,10 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
154
154
  'multiview_to_model',
155
155
  'model_segment',
156
156
  ]);
157
- if (!imageKinds.has(kind) && !modelKinds.has(kind) && !materialKinds.has(kind)) {
157
+ if (kind === 'texture-recipe' && !['gemini', 'openai'].includes(provider)) {
158
+ throw Object.assign(new Error('Texture recipes require Gemini or OpenAI.'), { statusCode: 400 });
159
+ }
160
+ if (kind !== 'texture-recipe' && !imageKinds.has(kind) && !modelKinds.has(kind) && !materialKinds.has(kind)) {
158
161
  throw Object.assign(new Error('Unsupported generation kind.'), { statusCode: 400 });
159
162
  }
160
163
  const providerKinds = {
@@ -203,6 +206,7 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
203
206
  );
204
207
  const result = await runProvider(provider, {
205
208
  ...requestBody,
209
+ ...(kind === 'texture-recipe' ? { jsonMode: true, responseMimeType: 'application/json', maxOutputTokens: 4096 } : {}),
206
210
  kind,
207
211
  prompt,
208
212
  referenceImages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@call-me-sensei/toonlab",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
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)",
@@ -61,6 +61,7 @@
61
61
  "src/catalog/officialCatalog.js",
62
62
  "src/catalog/officialCatalogLod.js",
63
63
  "src/catalog/officialCatalogPlacement.js",
64
+ "src/catalog/officialCatalogRockPackage.js",
64
65
  "src/core",
65
66
  "src/environment",
66
67
  "src/ground-shader",
@@ -125,7 +126,11 @@
125
126
  "compose.yaml",
126
127
  "NPM-LIBRARY.md",
127
128
  "README.md",
128
- "LICENSE"
129
+ "LICENSE",
130
+ "agents/references/game-lifecycle.md",
131
+ "agents/examples/game-foundation.mjs",
132
+ "agents/skills/codex/lighting",
133
+ "agents/skills/claude/lighting"
129
134
  ],
130
135
  "exports": {
131
136
  ".": {
@@ -278,7 +283,8 @@
278
283
  "golden:capture": "node scripts/capture-golden-fixtures.mjs",
279
284
  "golden:compare": "node scripts/compare-golden-captures.mjs",
280
285
  "verify:rockgen": "node scripts/verify-rockgen.mjs",
281
- "verify:rock-generation-lab": "node scripts/verify-rock-generation-lab.mjs",
286
+ "verify:rock-dense-field-lod": "node scripts/verify-rock-dense-field-lod.mjs && node scripts/verify-official-catalog-lod.mjs",
287
+ "verify:rock-generation-lab": "node scripts/verify-rock-tool-behavior.mjs",
282
288
  "verify:rock-c7-library": "node scripts/verify-rock-c7-surface-library.mjs",
283
289
  "verify:rock-lab-lifecycle": "node scripts/verify-rock-lab-lifecycle.mjs",
284
290
  "verify:rock-shader": "node scripts/verify-rock-shader.mjs",
@@ -304,7 +310,8 @@
304
310
  "verify:fauna": "node scripts/verify-fauna.mjs",
305
311
  "verify:ambientfx": "node scripts/verify-ambientfx.mjs",
306
312
  "verify:vfxgen": "node scripts/verify-vfxgen.mjs",
307
- "verify:water": "node scripts/verify-watergen.mjs",
313
+ "verify:water-experiments": "node scripts/verify-water-experiments.mjs",
314
+ "verify:water": "node scripts/verify-watergen.mjs && node scripts/verify-water-physics.mjs && node scripts/verify-water-dynamics.mjs && node scripts/verify-water-beach-swash.mjs",
308
315
  "verify:texgen": "node scripts/verify-texgen.mjs",
309
316
  "verify:landscape": "node scripts/verify-landscape.mjs",
310
317
  "verify:lighting": "node scripts/verify-lighting.mjs",
@@ -315,9 +322,9 @@
315
322
  "verify:lab-asset-provenance": "node scripts/verify-lab-asset-provenance.mjs",
316
323
  "verify:lab-preview": "node scripts/verify-lab-preview.mjs",
317
324
  "verify:library-preview": "node scripts/verify-library-preview-coverage.mjs",
318
- "verify:public-labs": "node scripts/verify-public-lab-authoring.mjs",
319
- "verify:vegetation-labs": "node scripts/verify-vegetation-lab-readiness.mjs",
320
- "verify:water-lab": "node scripts/verify-water-lab-contract.mjs",
325
+ "verify:public-labs": "node scripts/verify-public-lab-authoring.mjs && node scripts/verify-lab-audit-regressions.mjs && node scripts/verify-lab-editing-regressions.mjs",
326
+ "verify:vegetation-labs": "node scripts/verify-vegetation-lab-readiness.mjs && node scripts/verify-vegetation-preview-runtime.mjs",
327
+ "verify:water-lab": "node scripts/verify-water-lab-contract.mjs && node scripts/verify-water-experiments.mjs",
321
328
  "verify:weather": "node scripts/verify-weather.mjs",
322
329
  "verify:climate": "node scripts/verify-climate.mjs",
323
330
  "verify:atmospheric-condition-lab": "node scripts/verify-atmospheric-condition-lab.mjs",
@@ -356,15 +363,15 @@
356
363
  "verify:tree-species-references": "node scripts/verify-tree-species-references.mjs",
357
364
  "verify:product-namespace": "node scripts/verify-product-namespace.mjs",
358
365
  "verify:skills": "node scripts/verify-agent-skills.mjs",
359
- "verify:docs": "node scripts/verify-package-documentation.mjs",
366
+ "verify:docs": "node scripts/verify-package-documentation.mjs && node scripts/generate-settings-reference.mjs --check && node scripts/verify-documentation-examples.mjs",
360
367
  "verify:api": "node scripts/verify-public-api.mjs",
361
368
  "verify:package": "node scripts/verify-package-boundary.mjs",
362
369
  "verify:style-contract": "node scripts/verify-style-contract.mjs",
363
370
  "verify:style-labels-package": "node scripts/verify-packaged-style-labels.mjs",
364
371
  "verify:collision-package": "node scripts/verify-packaged-world-collision.mjs",
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",
372
+ "verify:official-catalog": "node scripts/verify-official-catalog-provider.mjs && node scripts/verify-official-catalog-rock-package.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",
366
373
  "verify:system-style": "node scripts/verify-system-style-policy.mjs",
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",
374
+ "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:natural-rock-surfaces && 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",
368
375
  "verify:generation": "node scripts/verify-generation.mjs",
369
376
  "verify:post": "node scripts/verify-post.mjs",
370
377
  "verify:camera": "node scripts/verify-camera.mjs",
@@ -376,7 +383,7 @@
376
383
  "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",
377
384
  "verify:catalog": "node scripts/verify-catalog.mjs",
378
385
  "verify:assetlib": "node scripts/verify-assetlib.mjs",
379
- "verify:mcp": "node scripts/verify-mcp.mjs",
386
+ "verify:mcp": "node scripts/sync-mcp-tree-contract.mjs --check && node scripts/verify-mcp.mjs && node scripts/verify-packaged-mcp.mjs",
380
387
  "verify:local-database": "node scripts/verify-local-database.mjs",
381
388
  "verify:creation-revisions": "node scripts/verify-creation-revisions.mjs",
382
389
  "verify:gallery-database": "node scripts/verify-official-gallery-database.mjs",
@@ -386,20 +393,21 @@
386
393
  "thumbs:catalog": "node scripts/generate-catalog-thumbs.mjs",
387
394
  "thumbs:home": "node scripts/generate-home-shots.mjs",
388
395
  "prepublishOnly": "npm run verify:release",
389
- "verify:scatter": "node scripts/verify-scatter.mjs"
396
+ "verify:scatter": "node scripts/verify-scatter.mjs",
397
+ "verify:natural-rock-surfaces": "node scripts/verify-natural-rock-surfaces.mjs && node scripts/verify-natural-rock-replay.mjs"
390
398
  },
391
399
  "dependencies": {
392
400
  "3d-tiles-renderer": "^0.5.0",
393
401
  "pg": "^8.22.0"
394
402
  },
395
403
  "peerDependencies": {
396
- "@react-three/fiber": "^9.0.0",
397
404
  "@dimforge/rapier3d-compat": "^0.19.0",
398
405
  "@pixiv/three-vrm": "^3.5.4",
406
+ "@react-three/fiber": "^9.0.0",
407
+ "react": "^19.0.0",
399
408
  "three": "^0.185.1",
400
- "three-mesh-bvh": "^0.8.3",
401
- "three-stdlib": "^2.36.1",
402
- "react": "^19.0.0"
409
+ "three-mesh-bvh": "0.9.14",
410
+ "three-stdlib": "^2.36.1"
403
411
  },
404
412
  "peerDependenciesMeta": {
405
413
  "@react-three/fiber": {
@@ -436,9 +444,12 @@
436
444
  "react": "^19.2.7",
437
445
  "react-dom": "^19.2.7",
438
446
  "three": "^0.185.1",
439
- "three-mesh-bvh": "^0.8.3",
447
+ "three-mesh-bvh": "0.9.14",
440
448
  "three-stdlib": "^2.36.1",
441
449
  "typescript": "^5.9.3",
442
- "vite": "^5.0.0"
450
+ "vite": "^5.0.0",
451
+ "manifold-3d": "3.5.1",
452
+ "meshoptimizer": "1.1.1",
453
+ "three-bvh-csg": "0.0.18"
443
454
  }
444
455
  }
@@ -2,3 +2,4 @@ export * from './officialCatalogProvider.js';
2
2
  export * from './officialCatalogAssetRuntime.js';
3
3
  export * from './officialCatalogLod.js';
4
4
  export * from './officialCatalogPlacement.js';
5
+ export * from './officialCatalogRockPackage.js';
@@ -14,6 +14,10 @@ import {
14
14
  rockSemanticMaterialTextures,
15
15
  } from '../rock-shader/rockSemanticMaterialRuntime.js';
16
16
  import { OFFICIAL_CATALOG_ASSET_VERSION } from './officialCatalogProvider.js';
17
+ import {
18
+ loadOfficialCatalogRockPackage,
19
+ loadOfficialCatalogRockShaderInput,
20
+ } from './officialCatalogRockPackage.js';
17
21
 
18
22
  const rendererPools = new WeakMap();
19
23
  const headlessPools = new Map();
@@ -141,18 +145,25 @@ async function defaultPrepareTextures(root, asset) {
141
145
  export function createOfficialCatalogAssetRuntime({
142
146
  decoderBasePath = '/',
143
147
  loadModel = loadModelAsset,
148
+ loadRockPackage = loadOfficialCatalogRockPackage,
149
+ loadRockShaderInput = loadOfficialCatalogRockShaderInput,
144
150
  prepareTextures = defaultPrepareTextures,
145
151
  provider,
146
152
  renderer = null,
153
+ rockTextureLoader = null,
147
154
  transcodersFactory = createModelAssetTranscoders,
148
155
  } = {}) {
149
156
  if (!provider || typeof provider.getAsset !== 'function') {
150
157
  throw new TypeError('Official catalog asset runtime requires a provider with getAsset().');
151
158
  }
152
159
  if (typeof loadModel !== 'function') throw new TypeError('loadModel must be a function.');
160
+ if (typeof loadRockPackage !== 'function') throw new TypeError('loadRockPackage must be a function.');
161
+ if (typeof loadRockShaderInput !== 'function') throw new TypeError('loadRockShaderInput must be a function.');
153
162
  if (typeof prepareTextures !== 'function') throw new TypeError('prepareTextures must be a function.');
154
163
 
155
164
  const assetPromises = new Map();
165
+ const rockPackagePromises = new Map();
166
+ const rockShaderInputPromises = new Map();
156
167
  const sourcePromises = new Map();
157
168
  const sourceEntries = new Map();
158
169
  const handles = new Set();
@@ -190,6 +201,53 @@ export function createOfficialCatalogAssetRuntime({
190
201
  return assetPromises.get(id);
191
202
  }
192
203
 
204
+ async function getAsset(assetOrId) {
205
+ assertActive();
206
+ return resolveAsset(assetOrId);
207
+ }
208
+
209
+ async function getRockPackage(assetOrId, options = {}) {
210
+ assertActive();
211
+ const asset = await resolveAsset(assetOrId);
212
+ assertActive();
213
+ const includeManifest = options.includeManifest === true;
214
+ const includeNatureProvenance = options.includeNatureProvenance === true;
215
+ const key = `${asset.identity}|manifest:${includeManifest}|nature:${includeNatureProvenance}`;
216
+ if (!rockPackagePromises.has(key)) {
217
+ const promise = Promise.resolve(loadRockPackage(asset, options)).catch((error) => {
218
+ if (rockPackagePromises.get(key) === promise) rockPackagePromises.delete(key);
219
+ throw error;
220
+ });
221
+ rockPackagePromises.set(key, promise);
222
+ }
223
+ return rockPackagePromises.get(key);
224
+ }
225
+
226
+ async function getRockShaderInput(assetOrId) {
227
+ assertActive();
228
+ const asset = await resolveAsset(assetOrId);
229
+ assertActive();
230
+ if (!asset.packageFiles?.materialConfigUrl) return null;
231
+ const key = asset.identity;
232
+ if (!rockShaderInputPromises.has(key)) {
233
+ const promise = (async () => {
234
+ const rockPackage = await getRockPackage(asset);
235
+ const options = rockTextureLoader ? { textureLoader: rockTextureLoader } : {};
236
+ const input = await loadRockShaderInput(rockPackage, options);
237
+ if (disposed) {
238
+ Object.values(input?.textures ?? {}).forEach((texture) => texture.dispose?.());
239
+ throw new Error('Official catalog asset runtime was disposed while loading rock textures.');
240
+ }
241
+ return input;
242
+ })().catch((error) => {
243
+ if (rockShaderInputPromises.get(key) === promise) rockShaderInputPromises.delete(key);
244
+ throw error;
245
+ });
246
+ rockShaderInputPromises.set(key, promise);
247
+ }
248
+ return rockShaderInputPromises.get(key);
249
+ }
250
+
193
251
  async function sourceFor(asset) {
194
252
  const key = asset.identity;
195
253
  if (!sourcePromises.has(key)) {
@@ -273,7 +331,26 @@ export function createOfficialCatalogAssetRuntime({
273
331
 
274
332
  function evict(assetOrId, { force = false } = {}) {
275
333
  const entry = findEntry(assetOrId);
276
- if (!entry) return false;
334
+ const identity = entry?.asset.identity ?? String(assetOrId?.identity ?? '');
335
+ const id = entry?.asset.id ?? String(assetOrId?.id ?? assetOrId ?? '');
336
+ const packageKeys = [...rockPackagePromises.keys()].filter((key) => (
337
+ (identity && key.startsWith(`${identity}|`)) || (!identity && key.includes(`:${id}@`))
338
+ ));
339
+ const shaderKeys = [...rockShaderInputPromises.keys()].filter((key) => (
340
+ key === identity || (!identity && key.includes(`:${id}@`))
341
+ ));
342
+ if (!entry && packageKeys.length === 0 && shaderKeys.length === 0) return false;
343
+ if (!entry) {
344
+ packageKeys.forEach((key) => rockPackagePromises.delete(key));
345
+ for (const key of shaderKeys) {
346
+ rockShaderInputPromises.get(key)?.then((input) => {
347
+ Object.values(input?.textures ?? {}).forEach((texture) => texture.dispose?.());
348
+ }).catch(() => {});
349
+ rockShaderInputPromises.delete(key);
350
+ }
351
+ assetPromises.delete(id);
352
+ return true;
353
+ }
277
354
  if (entry.refs > 0 && !force) return false;
278
355
  if (force) {
279
356
  for (const handle of [...handles]) {
@@ -284,6 +361,13 @@ export function createOfficialCatalogAssetRuntime({
284
361
  sourceEntries.delete(entry.asset.identity);
285
362
  sourcePromises.delete(entry.asset.identity);
286
363
  assetPromises.delete(entry.asset.id);
364
+ packageKeys.forEach((key) => rockPackagePromises.delete(key));
365
+ for (const key of shaderKeys) {
366
+ rockShaderInputPromises.get(key)?.then((input) => {
367
+ Object.values(input?.textures ?? {}).forEach((texture) => texture.dispose?.());
368
+ }).catch(() => {});
369
+ rockShaderInputPromises.delete(key);
370
+ }
287
371
  return true;
288
372
  }
289
373
 
@@ -297,6 +381,8 @@ export function createOfficialCatalogAssetRuntime({
297
381
  refs: entry.refs,
298
382
  }))),
299
383
  pendingAssets: sourcePromises.size - sourceEntries.size,
384
+ pendingRockPackages: rockPackagePromises.size,
385
+ cachedRockShaderInputs: rockShaderInputPromises.size,
300
386
  });
301
387
  }
302
388
 
@@ -305,10 +391,18 @@ export function createOfficialCatalogAssetRuntime({
305
391
  disposed = true;
306
392
  for (const handle of [...handles]) handle.release();
307
393
  await Promise.allSettled(sourcePromises.values());
394
+ const shaderInputs = await Promise.allSettled(rockShaderInputPromises.values());
395
+ for (const result of shaderInputs) {
396
+ if (result.status === 'fulfilled') {
397
+ Object.values(result.value?.textures ?? {}).forEach((texture) => texture.dispose?.());
398
+ }
399
+ }
308
400
  for (const entry of sourceEntries.values()) disposeResources(entry.resources);
309
401
  sourceEntries.clear();
310
402
  sourcePromises.clear();
311
403
  assetPromises.clear();
404
+ rockPackagePromises.clear();
405
+ rockShaderInputPromises.clear();
312
406
  transcoderLease?.release();
313
407
  transcoderLease = null;
314
408
  }
@@ -317,6 +411,9 @@ export function createOfficialCatalogAssetRuntime({
317
411
  acquireAsset,
318
412
  dispose,
319
413
  evict,
414
+ getAsset,
415
+ getRockPackage,
416
+ getRockShaderInput,
320
417
  get disposed() { return disposed; },
321
418
  provider,
322
419
  stats,
@@ -1,4 +1,4 @@
1
- import { Vector3 } from 'three';
1
+ import { Box3, Sphere, Vector3 } from 'three';
2
2
 
3
3
  const DEFAULT_LOD_NAME_PATTERN = /(?:^|[_\s.-])LOD[_\s.-]?(\d+)(?:$|[_\s.-])/iu;
4
4
 
@@ -97,16 +97,118 @@ export function selectCatalogLodLevel({
97
97
  return selected;
98
98
  }
99
99
 
100
+ function normalizePixelThresholds(thresholds, levelCount) {
101
+ const count = Math.max(Math.round(Number(levelCount) || 1), 1);
102
+ const source = Array.isArray(thresholds) ? thresholds : [];
103
+ const normalized = [];
104
+ for (let level = 0; level < count; level += 1) {
105
+ const fallback = level === 0 ? 240 : Math.max((normalized[level - 1] ?? 240) * 0.45, 1);
106
+ const value = Number(source[level]);
107
+ normalized.push(Number.isFinite(value) && value > 0 ? value : fallback);
108
+ }
109
+ for (let level = 1; level < normalized.length; level += 1) {
110
+ normalized[level] = Math.min(normalized[level], normalized[level - 1] - 0.001);
111
+ }
112
+ return Object.freeze(normalized);
113
+ }
114
+
115
+ /**
116
+ * Resolve an authored LOD from its projected on-screen diameter. A null result
117
+ * is an intentional cull decision, not a missing-asset failure.
118
+ */
119
+ export function selectCatalogLodLevelByProjectedPixels({
120
+ availableLevels,
121
+ cullBelowPixels = 0,
122
+ currentLevel = null,
123
+ hysteresis = 0,
124
+ pixelThresholds,
125
+ projectedPixels,
126
+ } = {}) {
127
+ const available = [...new Set((availableLevels ?? []).map(finiteLevel).filter((v) => v !== null))]
128
+ .sort((left, right) => left - right);
129
+ if (available.length === 0) return null;
130
+ const pixels = Number(projectedPixels);
131
+ if (!Number.isFinite(pixels) || pixels < 0) {
132
+ throw new RangeError('Projected LOD pixels must be a finite non-negative number.');
133
+ }
134
+ const margin = Number(hysteresis);
135
+ if (!Number.isFinite(margin) || margin < 0 || margin >= 0.5) {
136
+ throw new RangeError('Catalog LOD hysteresis must be a finite ratio in [0, 0.5).');
137
+ }
138
+ const cull = Math.max(Number(cullBelowPixels) || 0, 0);
139
+ const thresholds = normalizePixelThresholds(pixelThresholds, available.at(-1) + 1);
140
+ const current = finiteLevel(currentLevel);
141
+
142
+ if (pixels < cull) {
143
+ if (current !== null && available.includes(current) && margin > 0
144
+ && pixels >= cull * (1 - margin)) return current;
145
+ return null;
146
+ }
147
+
148
+ let requested = thresholds.length - 1;
149
+ for (let level = 0; level < thresholds.length; level += 1) {
150
+ if (pixels >= thresholds[level]) {
151
+ requested = level;
152
+ break;
153
+ }
154
+ }
155
+ const selected = available.filter((level) => level <= requested).at(-1) ?? available[0];
156
+ if (current === null || !available.includes(current) || margin === 0 || selected === current) {
157
+ return selected;
158
+ }
159
+
160
+ if (selected > current) {
161
+ const boundary = thresholds[current] ?? cull;
162
+ if (pixels >= boundary * (1 - margin)) return current;
163
+ } else {
164
+ const boundary = thresholds[selected] ?? thresholds[0];
165
+ if (pixels <= boundary * (1 + margin)) return current;
166
+ }
167
+ return selected;
168
+ }
169
+
170
+ /** Estimate projected object diameter for perspective and orthographic cameras. */
171
+ export function estimateCatalogProjectedPixels({
172
+ camera,
173
+ distance,
174
+ referenceDiameter,
175
+ viewportHeight,
176
+ } = {}) {
177
+ const pixelsHigh = Number(viewportHeight);
178
+ const diameter = Number(referenceDiameter);
179
+ if (!camera?.isCamera) throw new TypeError('Projected LOD estimation requires a Three.js camera.');
180
+ if (!Number.isFinite(pixelsHigh) || pixelsHigh <= 0) {
181
+ throw new RangeError('Projected LOD estimation requires a positive viewportHeight.');
182
+ }
183
+ if (!Number.isFinite(diameter) || diameter <= 0) {
184
+ throw new RangeError('Projected LOD estimation requires a positive referenceDiameter.');
185
+ }
186
+ if (camera.isOrthographicCamera) {
187
+ const span = Math.abs(Number(camera.top) - Number(camera.bottom)) / Math.max(Number(camera.zoom) || 1, 0.001);
188
+ return span > 0 ? diameter / span * pixelsHigh : Number.POSITIVE_INFINITY;
189
+ }
190
+ if (!camera.isPerspectiveCamera) {
191
+ throw new TypeError('Projected LOD estimation supports perspective and orthographic cameras.');
192
+ }
193
+ const range = Math.max(Number(distance) || 0, 0.0001);
194
+ const zoom = Math.max(Number(camera.zoom) || 1, 0.001);
195
+ const halfFov = Math.atan(Math.tan((Number(camera.fov) || 50) * Math.PI / 360) / zoom);
196
+ return diameter / (2 * range * Math.tan(halfFov)) * pixelsHigh;
197
+ }
198
+
100
199
  /**
101
200
  * Authored, renderer-independent LOD controller. The host calls update() with
102
201
  * either a direct distance or a camera. World scale is removed from camera
103
202
  * distance so a uniformly enlarged landmark changes LOD at proportional range.
104
203
  */
105
204
  export function createCatalogLodRuntime(root, {
205
+ cullBelowPixels = 0,
106
206
  distances = [0, 45, 120],
107
207
  hysteresis = 0,
108
208
  maxLevel = Number.POSITIVE_INFINITY,
109
209
  namePattern = DEFAULT_LOD_NAME_PATTERN,
210
+ pixelThresholds = null,
211
+ referenceDiameter = null,
110
212
  } = {}) {
111
213
  const bindings = collectCatalogLodBindings(root, { namePattern });
112
214
  const availableLevels = Object.freeze(
@@ -119,27 +221,41 @@ export function createCatalogLodRuntime(root, {
119
221
  }
120
222
  const worldPosition = new Vector3();
121
223
  const worldScale = new Vector3();
224
+ const bounds = new Box3();
225
+ const sphere = new Sphere();
226
+ const usesScreenSpace = Array.isArray(pixelThresholds);
227
+ let localReferenceDiameter = Number(referenceDiameter);
228
+ if (!Number.isFinite(localReferenceDiameter) || localReferenceDiameter <= 0) {
229
+ root.updateWorldMatrix?.(true, true);
230
+ bounds.setFromObject(root, true).getBoundingSphere(sphere);
231
+ root.getWorldScale(worldScale);
232
+ const scale = Math.max(Math.abs(worldScale.x), Math.abs(worldScale.y), Math.abs(worldScale.z), 0.001);
233
+ localReferenceDiameter = Math.max(sphere.radius * 2 / scale, 0.001);
234
+ }
235
+ const normalizedPixelThresholds = usesScreenSpace
236
+ ? normalizePixelThresholds(pixelThresholds, availableLevels.at(-1) + 1)
237
+ : null;
122
238
  let currentLevel = null;
123
239
  let disposed = false;
124
240
 
125
241
  function setLevel(level) {
126
- if (disposed || level === null || !availableLevels.includes(level)) return false;
242
+ if (disposed || (level !== null && !availableLevels.includes(level))) return false;
127
243
  if (currentLevel === level) return false;
128
244
  bindings.forEach((binding) => {
129
- binding.mesh.visible = binding.level === level;
245
+ binding.mesh.visible = level !== null && binding.level === level;
130
246
  });
131
247
  currentLevel = level;
132
248
  return true;
133
249
  }
134
250
 
135
251
  function resolveDistance({ camera = null, distance = null } = {}) {
252
+ root.updateWorldMatrix?.(true, false);
253
+ root.getWorldPosition(worldPosition);
254
+ root.getWorldScale(worldScale);
136
255
  if (Number.isFinite(distance)) return Math.max(distance, 0);
137
256
  if (!camera?.getWorldPosition && !camera?.position) {
138
257
  throw new TypeError('Catalog LOD update requires distance or camera.');
139
258
  }
140
- root.updateWorldMatrix?.(true, false);
141
- root.getWorldPosition(worldPosition);
142
- root.getWorldScale(worldScale);
143
259
  const cameraPosition = camera.getWorldPosition
144
260
  ? camera.getWorldPosition(new Vector3())
145
261
  : camera.position;
@@ -149,17 +265,42 @@ export function createCatalogLodRuntime(root, {
149
265
 
150
266
  function update(options = {}) {
151
267
  if (disposed || availableLevels.length === 0) return null;
152
- const distance = resolveDistance(options);
153
- const level = selectCatalogLodLevel({
154
- availableLevels,
155
- distance,
156
- distances: thresholds,
157
- currentLevel,
158
- hysteresis: hysteresisRatio,
159
- maxLevel,
160
- });
268
+ const hasDirectPixels = Number.isFinite(Number(options.projectedPixels));
269
+ const distance = hasDirectPixels && !Number.isFinite(options.distance) && !options.camera
270
+ ? null
271
+ : resolveDistance(options);
272
+ const projectedPixels = hasDirectPixels
273
+ ? Math.max(Number(options.projectedPixels), 0)
274
+ : usesScreenSpace
275
+ ? estimateCatalogProjectedPixels({
276
+ camera: options.camera,
277
+ distance,
278
+ referenceDiameter: localReferenceDiameter * Math.max(
279
+ Math.abs(worldScale.x), Math.abs(worldScale.y), Math.abs(worldScale.z), 0.001,
280
+ ),
281
+ viewportHeight: options.viewportHeight,
282
+ })
283
+ : null;
284
+ let level = usesScreenSpace
285
+ ? selectCatalogLodLevelByProjectedPixels({
286
+ availableLevels,
287
+ cullBelowPixels,
288
+ currentLevel,
289
+ hysteresis: hysteresisRatio,
290
+ pixelThresholds: normalizedPixelThresholds,
291
+ projectedPixels,
292
+ })
293
+ : selectCatalogLodLevel({
294
+ availableLevels,
295
+ distance,
296
+ distances: thresholds,
297
+ currentLevel,
298
+ hysteresis: hysteresisRatio,
299
+ maxLevel,
300
+ });
301
+ if (level !== null) level = Math.min(level, Math.max(Number(maxLevel) || 0, 0));
161
302
  const changed = setLevel(level);
162
- return Object.freeze({ changed, distance, level });
303
+ return Object.freeze({ changed, culled: level === null, distance, level, projectedPixels });
163
304
  }
164
305
 
165
306
  if (availableLevels.length > 0) setLevel(availableLevels[0]);
@@ -176,6 +317,9 @@ export function createCatalogLodRuntime(root, {
176
317
  get disposed() { return disposed; },
177
318
  get level() { return currentLevel; },
178
319
  setLevel,
320
+ cullBelowPixels: Math.max(Number(cullBelowPixels) || 0, 0),
321
+ pixelThresholds: normalizedPixelThresholds,
322
+ referenceDiameter: localReferenceDiameter,
179
323
  thresholds,
180
324
  hysteresis: hysteresisRatio,
181
325
  update,