@call-me-sensei/toonlab 0.4.21 → 0.4.22

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 (31) hide show
  1. package/NPM-LIBRARY.md +51 -2
  2. package/README.md +4 -3
  3. package/agents/PROMPTS.md +1 -1
  4. package/database/apply-one-catalog-seed.mjs +63 -0
  5. package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
  6. package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
  7. package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
  8. package/package.json +4 -2
  9. package/src/catalog/officialCatalog.js +1 -0
  10. package/src/catalog/officialCatalogAssetRuntime.js +98 -1
  11. package/src/catalog/officialCatalogLod.js +160 -16
  12. package/src/catalog/officialCatalogPlacement.js +52 -12
  13. package/src/catalog/officialCatalogProvider.js +79 -8
  14. package/src/catalog/officialCatalogRockPackage.js +172 -0
  15. package/src/rock-shader/rockMaterial.js +205 -28
  16. package/src/rock-shader/rockShaderRuntime.js +17 -1
  17. package/src/rock-shader/rockShaderSettings.js +16 -0
  18. package/src/rockgen/lod/index.js +1 -0
  19. package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
  20. package/src/rockgen/rockDocument.js +604 -26
  21. package/src/version.js +1 -1
  22. package/types/catalog/officialCatalog.d.ts +1 -0
  23. package/types/catalog/officialCatalogPlacement.d.ts +4 -0
  24. package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
  25. package/types/index.d.ts +24 -0
  26. package/types/rock-shader/rockMaterial.d.ts +3 -0
  27. package/types/rock-shader/rockShaderSettings.d.ts +2 -0
  28. package/types/rockgen/lod/index.d.ts +1 -0
  29. package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
  30. package/types/rockgen/rockDocument.d.ts +579 -68
  31. package/types/version.d.ts +1 -1
@@ -34,6 +34,20 @@ const GROUP_DEFINITIONS = Object.freeze({
34
34
  description: 'World-space base projection and graphic texture treatment.',
35
35
  label: 'Base Projection',
36
36
  fields: Object.freeze({
37
+ mode: field({
38
+ defaultValue: 'triplanar',
39
+ description: 'Compile-time texture graph: ordinary three-axis projection or coherent bedding that excludes the top-axis structural sample.',
40
+ label: 'Projection Mode',
41
+ options: ['triplanar', 'directional-bedding'],
42
+ type: 'select',
43
+ }),
44
+ upAxis: field({
45
+ defaultValue: 'y',
46
+ description: 'World-space geological up axis used by directional bedding.',
47
+ label: 'Bedding Up Axis',
48
+ options: ['x', 'y', 'z'],
49
+ type: 'select',
50
+ }),
37
51
  scale: field({
38
52
  defaultValue: 1.6,
39
53
  description: 'World-space size of the projected rock texture in meters.',
@@ -792,6 +806,8 @@ export const DEFAULT_ROCK_SHADER_SETTINGS = Object.freeze(Object.fromEntries(
792
806
  // visible, serializable, and independent from any preview fixture.
793
807
  export const CALL_ME_SENSEI_ROCK_SHADER_SETTINGS = Object.freeze({
794
808
  projection: Object.freeze({
809
+ mode: 'triplanar',
810
+ upAxis: 'y',
795
811
  scale: 48,
796
812
  // Geological maps already own lithology color and value structure. The
797
813
  // previous 0.72 saturation/contrast pair washed every family toward the
@@ -1,4 +1,5 @@
1
1
  export * from './rockLodMetrics.js';
2
+ export * from './rockDenseFieldPolicy.js';
2
3
  export * from './rockLodPlanner.js';
3
4
  export * from './rockLodPolicy.js';
4
5
  export * from './rockLodRuntime.js';
@@ -0,0 +1,143 @@
1
+ import {
2
+ collectCatalogLodBindings,
3
+ createCatalogLodRuntime,
4
+ } from '../../catalog/officialCatalogLod.js';
5
+
6
+ export const DENSE_FIELD_ROCK_LOD_LEVELS = Object.freeze([
7
+ Object.freeze({ id: 'LOD0', level: 0, purpose: 'hero-close' }),
8
+ Object.freeze({ id: 'LOD1', level: 1, purpose: 'near-gameplay' }),
9
+ Object.freeze({ id: 'LOD2', level: 2, purpose: 'mid-gameplay' }),
10
+ Object.freeze({ id: 'LOD3', level: 3, purpose: 'far-simplified' }),
11
+ Object.freeze({ id: 'LOD4', level: 4, purpose: 'very-far-silhouette' }),
12
+ ]);
13
+
14
+ export const DENSE_FIELD_ROCK_PIXEL_THRESHOLDS = Object.freeze([240, 110, 48, 16, 6]);
15
+ export const DENSE_FIELD_ROCK_CULL_BELOW_PIXELS = 5;
16
+
17
+ export const DENSE_FIELD_ROCK_TRIANGLE_CEILINGS = Object.freeze({
18
+ ordinary: Object.freeze({ LOD3: 1200, LOD4: 350 }),
19
+ formation: Object.freeze({ LOD3: 1800, LOD4: 600 }),
20
+ landmark: Object.freeze({ LOD3: 2500, LOD4: 800 }),
21
+ });
22
+
23
+ export const DENSE_FIELD_ROCK_PRODUCTION_RULE = Object.freeze({
24
+ cullBelowPixels: DENSE_FIELD_ROCK_CULL_BELOW_PIXELS,
25
+ farShadowLastLevel: 2,
26
+ levelCount: 5,
27
+ levels: DENSE_FIELD_ROCK_LOD_LEVELS,
28
+ pixelThresholds: DENSE_FIELD_ROCK_PIXEL_THRESHOLDS,
29
+ selectionBasis: 'projected-object-diameter-pixels',
30
+ triangleCeilings: DENSE_FIELD_ROCK_TRIANGLE_CEILINGS,
31
+ veryFar: Object.freeze({
32
+ allowedSurfaceChannels: Object.freeze(['baseColor', 'broadLighting']),
33
+ castShadow: false,
34
+ preserve: Object.freeze(['outerSilhouette', 'primaryMass', 'groundContact', 'broadBaseColor']),
35
+ remove: Object.freeze([
36
+ 'cracks',
37
+ 'strataRelief',
38
+ 'cavities',
39
+ 'secondaryLedges',
40
+ 'normalMap',
41
+ 'heightMap',
42
+ 'aoMap',
43
+ 'uniqueRoughness',
44
+ ]),
45
+ silhouetteOnly: true,
46
+ }),
47
+ });
48
+
49
+ export function inferDenseFieldRockClass({ scaleClass = null, targetHeightMetres = null } = {}) {
50
+ const named = String(scaleClass ?? '').trim().toLowerCase();
51
+ if (named === 'landmark' || named === 'hero' || named === 'backdrop') return 'landmark';
52
+ if (named === 'formation' || named === 'outcrop' || named === 'cliff') return 'formation';
53
+ if (named === 'ordinary' || named === 'boulder' || named === 'debris') return 'ordinary';
54
+ const height = Number(targetHeightMetres);
55
+ if (Number.isFinite(height) && height > 6) return 'landmark';
56
+ if (Number.isFinite(height) && height > 2) return 'formation';
57
+ return 'ordinary';
58
+ }
59
+
60
+ export function validateDenseFieldRockLodTargets(targets, {
61
+ assetClass = 'ordinary',
62
+ } = {}) {
63
+ const classification = Object.hasOwn(DENSE_FIELD_ROCK_TRIANGLE_CEILINGS, assetClass)
64
+ ? assetClass
65
+ : 'ordinary';
66
+ const ceilings = DENSE_FIELD_ROCK_TRIANGLE_CEILINGS[classification];
67
+ const values = DENSE_FIELD_ROCK_LOD_LEVELS.map(({ id }) => Number(targets?.[id]));
68
+ const errors = [];
69
+ values.forEach((value, level) => {
70
+ if (!Number.isInteger(value) || value <= 0) {
71
+ errors.push(`${DENSE_FIELD_ROCK_LOD_LEVELS[level].id} must be a positive integer`);
72
+ }
73
+ });
74
+ for (let level = 1; level < values.length; level += 1) {
75
+ if (Number.isFinite(values[level]) && Number.isFinite(values[level - 1])
76
+ && values[level] >= values[level - 1]) {
77
+ errors.push(`LOD${level} must contain fewer triangles than LOD${level - 1}`);
78
+ }
79
+ }
80
+ if (Number.isFinite(values[3]) && values[3] > ceilings.LOD3) {
81
+ errors.push(`LOD3 exceeds the ${classification} ${ceilings.LOD3}-triangle ceiling`);
82
+ }
83
+ if (Number.isFinite(values[4]) && values[4] > ceilings.LOD4) {
84
+ errors.push(`LOD4 exceeds the ${classification} ${ceilings.LOD4}-triangle ceiling`);
85
+ }
86
+ return Object.freeze({
87
+ assetClass: classification,
88
+ ceilings,
89
+ errors: Object.freeze(errors),
90
+ valid: errors.length === 0,
91
+ values: Object.freeze(values),
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Install the mandatory dense-field rock behavior on an authored LOD0-LOD4
97
+ * root. The final tier is geometry-only silhouette support; it never casts a
98
+ * shadow and the root is culled once its projected diameter drops below the
99
+ * production pixel threshold.
100
+ */
101
+ export function createDenseFieldRockLodRuntime(root, {
102
+ cullBelowPixels = DENSE_FIELD_ROCK_CULL_BELOW_PIXELS,
103
+ hysteresis = 0.12,
104
+ maxLevel = Number.POSITIVE_INFINITY,
105
+ pixelThresholds = DENSE_FIELD_ROCK_PIXEL_THRESHOLDS,
106
+ referenceDiameter = null,
107
+ requireComplete = true,
108
+ } = {}) {
109
+ const bindings = collectCatalogLodBindings(root);
110
+ const levels = [...new Set(bindings.map(({ level }) => level))].sort((a, b) => a - b);
111
+ const missing = DENSE_FIELD_ROCK_LOD_LEVELS
112
+ .map(({ level }) => level)
113
+ .filter((level) => !levels.includes(level));
114
+ if (requireComplete && missing.length > 0) {
115
+ throw new Error(`Dense-field rock is missing required visual tiers: ${missing.map((level) => `LOD${level}`).join(', ')}`);
116
+ }
117
+
118
+ const originalShadows = new Map(bindings.map(({ mesh }) => [mesh, mesh.castShadow]));
119
+ for (const { level, mesh } of bindings) {
120
+ if (level > DENSE_FIELD_ROCK_PRODUCTION_RULE.farShadowLastLevel) mesh.castShadow = false;
121
+ mesh.userData.toonlabDenseFieldRock = Object.freeze({
122
+ castShadow: mesh.castShadow,
123
+ silhouetteOnly: level === 4,
124
+ visualLevel: level,
125
+ });
126
+ }
127
+
128
+ const runtime = createCatalogLodRuntime(root, {
129
+ cullBelowPixels,
130
+ hysteresis,
131
+ maxLevel,
132
+ pixelThresholds,
133
+ referenceDiameter,
134
+ });
135
+ return Object.freeze({
136
+ ...runtime,
137
+ policy: DENSE_FIELD_ROCK_PRODUCTION_RULE,
138
+ dispose() {
139
+ runtime.dispose();
140
+ for (const [mesh, castShadow] of originalShadows) mesh.castShadow = castShadow;
141
+ },
142
+ });
143
+ }