@jgengine/core 0.19.0 → 0.22.0

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 (266) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/ai/decisionGraph.d.ts +2 -0
  3. package/dist/ai/decisionGraph.js +6 -0
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/anim/animGraph.d.ts +33 -2
  7. package/dist/anim/animGraph.js +200 -12
  8. package/dist/anim/locomotionGraph.d.ts +11 -3
  9. package/dist/anim/locomotionGraph.js +25 -15
  10. package/dist/audio/audioFalloff.d.ts +4 -0
  11. package/dist/audio/voiceAllocator.d.ts +46 -0
  12. package/dist/audio/voiceAllocator.js +76 -0
  13. package/dist/combat/abilityKit.d.ts +19 -0
  14. package/dist/combat/abilityKit.js +40 -4
  15. package/dist/combat/death.d.ts +4 -3
  16. package/dist/combat/death.js +3 -3
  17. package/dist/combat/magazine.d.ts +9 -0
  18. package/dist/combat/magazine.js +24 -2
  19. package/dist/combat/projectiles.d.ts +94 -2
  20. package/dist/combat/projectiles.js +282 -61
  21. package/dist/combat/regenShield.d.ts +14 -0
  22. package/dist/combat/regenShield.js +23 -7
  23. package/dist/crafting/recipeCatalog.d.ts +16 -0
  24. package/dist/crafting/recipeCatalog.js +55 -0
  25. package/dist/economy/auctionBook.d.ts +1 -1
  26. package/dist/economy/auctionBook.js +11 -1
  27. package/dist/economy/listingBook.d.ts +1 -1
  28. package/dist/economy/listingBook.js +11 -1
  29. package/dist/economy/resourceLedger.js +3 -3
  30. package/dist/economy/shopStock.js +7 -6
  31. package/dist/economy/techTree.js +7 -6
  32. package/dist/economy/unlockPoints.js +3 -6
  33. package/dist/editor/commandApply.js +89 -16
  34. package/dist/editor/commands.d.ts +38 -1
  35. package/dist/editor/commands.js +320 -3
  36. package/dist/editor/creatorStorage.d.ts +59 -0
  37. package/dist/editor/creatorStorage.js +164 -0
  38. package/dist/editor/document.d.ts +4 -0
  39. package/dist/editor/document.js +116 -17
  40. package/dist/editor/index.d.ts +3 -0
  41. package/dist/editor/index.js +1 -0
  42. package/dist/editor/liveSync.d.ts +4 -1
  43. package/dist/editor/liveSync.js +26 -10
  44. package/dist/editor/materialAuthoring.d.ts +14 -0
  45. package/dist/editor/materialAuthoring.js +38 -0
  46. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  47. package/dist/editor/modelNodeVisibility.js +29 -0
  48. package/dist/editor/movementCatalog.d.ts +90 -0
  49. package/dist/editor/movementCatalog.js +144 -0
  50. package/dist/editor/simulation.d.ts +60 -0
  51. package/dist/editor/simulation.js +202 -0
  52. package/dist/editor/staticPrefab.d.ts +17 -0
  53. package/dist/editor/staticPrefab.js +51 -0
  54. package/dist/editor/terrainMaterials.d.ts +10 -0
  55. package/dist/editor/terrainMaterials.js +13 -0
  56. package/dist/editor/types.d.ts +11 -0
  57. package/dist/game/cameraConfig.d.ts +3 -1
  58. package/dist/game/clipRoles.d.ts +2 -1
  59. package/dist/game/clipRoles.js +7 -3
  60. package/dist/game/contentValidation.d.ts +69 -0
  61. package/dist/game/contentValidation.js +118 -0
  62. package/dist/game/controlGate.d.ts +8 -0
  63. package/dist/game/controlGate.js +43 -6
  64. package/dist/game/defineGame.d.ts +32 -3
  65. package/dist/game/dialogueGraph.d.ts +20 -0
  66. package/dist/game/dialogueGraph.js +107 -0
  67. package/dist/game/events.d.ts +5 -0
  68. package/dist/game/keyValueStore.d.ts +17 -4
  69. package/dist/game/keyValueStore.js +35 -23
  70. package/dist/game/lootModifiers.d.ts +17 -0
  71. package/dist/game/lootModifiers.js +26 -0
  72. package/dist/game/narrativeAuthoring.d.ts +67 -0
  73. package/dist/game/narrativeAuthoring.js +122 -0
  74. package/dist/game/playableGame.d.ts +52 -20
  75. package/dist/game/quest.d.ts +15 -0
  76. package/dist/game/quest.js +209 -63
  77. package/dist/game/questCatalog.d.ts +50 -0
  78. package/dist/game/questCatalog.js +205 -0
  79. package/dist/game/saveStore.d.ts +6 -5
  80. package/dist/game/saveStore.js +57 -57
  81. package/dist/game/systemRuntime.d.ts +2 -2
  82. package/dist/game/systemRuntime.js +84 -33
  83. package/dist/gameplay.d.ts +3 -0
  84. package/dist/gameplay.js +3 -0
  85. package/dist/interaction/promptRegistry.d.ts +5 -4
  86. package/dist/interaction/promptRegistry.js +8 -2
  87. package/dist/inventory/inventoryModel.js +9 -6
  88. package/dist/inventory/shapedGrid.d.ts +48 -9
  89. package/dist/inventory/shapedGrid.js +69 -10
  90. package/dist/item/durability.js +4 -0
  91. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  92. package/dist/item/itemInstanceRegistry.js +111 -7
  93. package/dist/item/itemgen.d.ts +1 -1
  94. package/dist/item/itemgen.js +1 -1
  95. package/dist/item/modularItem.js +7 -1
  96. package/dist/material/appearanceSignals.d.ts +53 -0
  97. package/dist/material/appearanceSignals.js +47 -0
  98. package/dist/material/hairCards.d.ts +47 -0
  99. package/dist/material/hairCards.js +136 -0
  100. package/dist/material/materialAsset.d.ts +162 -0
  101. package/dist/material/materialAsset.js +382 -0
  102. package/dist/meta/changelog.d.ts +1 -1
  103. package/dist/meta/changelog.js +348 -1
  104. package/dist/movement/characterController.d.ts +5 -2
  105. package/dist/movement/characterController.js +50 -23
  106. package/dist/movement/freeFlight.d.ts +2 -2
  107. package/dist/movement/freeFlight.js +3 -3
  108. package/dist/movement/movementModel.d.ts +24 -1
  109. package/dist/movement/movementModel.js +74 -7
  110. package/dist/movement/playerMovement.d.ts +28 -1
  111. package/dist/movement/playerMovement.js +425 -102
  112. package/dist/movement/poseState.d.ts +2 -0
  113. package/dist/movement/poseState.js +29 -1
  114. package/dist/movement/solidObstacles.d.ts +3 -1
  115. package/dist/movement/solidObstacles.js +8 -5
  116. package/dist/movement/terrainGrade.d.ts +13 -0
  117. package/dist/movement/terrainGrade.js +33 -0
  118. package/dist/movement/voxelController.d.ts +2 -2
  119. package/dist/movement/voxelController.js +3 -3
  120. package/dist/nav/navGrid.d.ts +19 -1
  121. package/dist/nav/navGrid.js +26 -10
  122. package/dist/nav/solidRoute.d.ts +50 -0
  123. package/dist/nav/solidRoute.js +144 -0
  124. package/dist/physics/ballisticSweep.d.ts +10 -3
  125. package/dist/physics/ballisticSweep.js +53 -29
  126. package/dist/physics/forceVolume.d.ts +41 -0
  127. package/dist/physics/forceVolume.js +88 -0
  128. package/dist/physics/kinematicVehicle.d.ts +8 -3
  129. package/dist/physics/kinematicVehicle.js +2 -2
  130. package/dist/physics/physicsWorldBackend.js +19 -3
  131. package/dist/physics/spatialGrid.d.ts +1 -1
  132. package/dist/physics/spatialGrid.js +8 -9
  133. package/dist/physics/vehicleDynamics.d.ts +3 -2
  134. package/dist/physics/vehicleDynamics.js +2 -2
  135. package/dist/physics/vehicleObstacles.d.ts +3 -2
  136. package/dist/physics/vehicleObstacles.js +29 -5
  137. package/dist/runtime/cameraDirector.d.ts +39 -2
  138. package/dist/runtime/cameraDirector.js +162 -0
  139. package/dist/runtime/commandDispatch.d.ts +14 -0
  140. package/dist/runtime/commandDispatch.js +19 -0
  141. package/dist/runtime/context/combat.d.ts +11 -4
  142. package/dist/runtime/context/combat.js +178 -11
  143. package/dist/runtime/context/deathLoot.d.ts +9 -8
  144. package/dist/runtime/context/deathLoot.js +16 -16
  145. package/dist/runtime/context/scene.d.ts +2 -0
  146. package/dist/runtime/context/scene.js +13 -21
  147. package/dist/runtime/descriptors/baseline.js +34 -3
  148. package/dist/runtime/descriptors/features.d.ts +2 -1
  149. package/dist/runtime/descriptors/features.js +18 -0
  150. package/dist/runtime/gameContext.js +79 -13
  151. package/dist/runtime/gameContextTypes.d.ts +14 -0
  152. package/dist/runtime/headlessRunner.d.ts +3 -1
  153. package/dist/runtime/headlessRunner.js +11 -11
  154. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  155. package/dist/runtime/hostedGameRunner.js +156 -10
  156. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  157. package/dist/runtime/hostedWorldSession.js +154 -16
  158. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  159. package/dist/runtime/inputRecorder.d.ts +1 -1
  160. package/dist/runtime/inputRecorder.js +21 -1
  161. package/dist/runtime/inputSnapshot.d.ts +16 -4
  162. package/dist/runtime/inputSnapshot.js +31 -3
  163. package/dist/runtime/runtimeSave.d.ts +10 -6
  164. package/dist/runtime/runtimeSave.js +11 -5
  165. package/dist/runtime/simContext.d.ts +8 -3
  166. package/dist/runtime/simContext.js +7 -3
  167. package/dist/runtime/simSnapshot.js +2 -0
  168. package/dist/runtime/worldReplication.d.ts +2 -0
  169. package/dist/runtime/worldReplication.js +1 -1
  170. package/dist/scene/assetCatalog.d.ts +2 -0
  171. package/dist/scene/behaviorRuntime.d.ts +26 -5
  172. package/dist/scene/behaviorRuntime.js +263 -16
  173. package/dist/scene/behaviors.d.ts +30 -1
  174. package/dist/scene/behaviors.js +29 -0
  175. package/dist/scene/entityStore.d.ts +14 -3
  176. package/dist/scene/entityStore.js +45 -9
  177. package/dist/scene/mount.d.ts +9 -0
  178. package/dist/scene/mount.js +35 -0
  179. package/dist/scene/possession.d.ts +2 -0
  180. package/dist/scene/possession.js +21 -1
  181. package/dist/scene/sceneRaycast.d.ts +7 -0
  182. package/dist/scene/sceneRaycast.js +55 -0
  183. package/dist/scene/spatial.d.ts +20 -1
  184. package/dist/scene/spatial.js +94 -2
  185. package/dist/scene/vehicleSeat.d.ts +36 -2
  186. package/dist/scene/vehicleSeat.js +89 -2
  187. package/dist/stats/accumulatorMeter.d.ts +10 -0
  188. package/dist/stats/accumulatorMeter.js +6 -0
  189. package/dist/stats/eventMeter.d.ts +9 -2
  190. package/dist/stats/eventMeter.js +5 -2
  191. package/dist/store/defineStore.d.ts +3 -1
  192. package/dist/store/defineStore.js +3 -1
  193. package/dist/store/observableKeyedStore.d.ts +1 -1
  194. package/dist/store/observableKeyedStore.js +8 -6
  195. package/dist/time/simClock.d.ts +6 -2
  196. package/dist/time/simClock.js +5 -0
  197. package/dist/vfx/particleDirector.d.ts +58 -6
  198. package/dist/vfx/particleDirector.js +113 -22
  199. package/dist/vfx/particles.d.ts +68 -5
  200. package/dist/vfx/particles.js +284 -32
  201. package/dist/visibility/assetStreaming.d.ts +21 -3
  202. package/dist/visibility/assetStreaming.js +108 -24
  203. package/dist/visibility/settings.d.ts +8 -2
  204. package/dist/visibility/settings.js +3 -0
  205. package/dist/visibility/visibilitySystem.js +1 -0
  206. package/dist/work/expedition.d.ts +110 -0
  207. package/dist/work/expedition.js +195 -0
  208. package/dist/work/staffedStation.d.ts +29 -0
  209. package/dist/work/staffedStation.js +27 -0
  210. package/dist/world/authoredAnimation.d.ts +38 -0
  211. package/dist/world/authoredAnimation.js +99 -0
  212. package/dist/world/authoredObjects.d.ts +24 -4
  213. package/dist/world/authoredObjects.js +43 -16
  214. package/dist/world/authoredSimulation.d.ts +74 -0
  215. package/dist/world/authoredSimulation.js +302 -0
  216. package/dist/world/authoredWeather.d.ts +120 -0
  217. package/dist/world/authoredWeather.js +314 -0
  218. package/dist/world/buildingKit.d.ts +8 -3
  219. package/dist/world/buildingKit.js +21 -8
  220. package/dist/world/environmentForces.d.ts +21 -0
  221. package/dist/world/environmentForces.js +42 -0
  222. package/dist/world/features.d.ts +3 -3
  223. package/dist/world/footprintGrid.d.ts +55 -0
  224. package/dist/world/footprintGrid.js +201 -0
  225. package/dist/world/grassKind.d.ts +10 -0
  226. package/dist/world/grassKind.js +22 -0
  227. package/dist/world/pathTerrain.d.ts +1 -1
  228. package/dist/world/terraform.d.ts +2 -2
  229. package/dist/world/terraform.js +46 -18
  230. package/dist/world/weather.d.ts +17 -0
  231. package/dist/world/weather.js +116 -20
  232. package/dist/world/worldSolids.d.ts +11 -0
  233. package/dist/world/worldSolids.js +112 -1
  234. package/dist/world.d.ts +5 -5
  235. package/dist/world.js +4 -4
  236. package/package.json +1 -1
  237. package/skills/ce-handoff/SKILL.md +1 -1
  238. package/skills/game-design/SKILL.md +2 -0
  239. package/skills/jgengine/SKILL.md +23 -2
  240. package/skills/jgengine/api.md +28 -17
  241. package/skills/jgengine/recipes/minimal-game.md +7 -2
  242. package/skills/jgengine-combat/SKILL.md +8 -2
  243. package/skills/jgengine-combat/api.md +22 -10
  244. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  245. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  246. package/skills/jgengine-combat/reference.md +24 -2
  247. package/skills/jgengine-gameplay/SKILL.md +63 -0
  248. package/skills/jgengine-gameplay/api.md +131 -37
  249. package/skills/jgengine-gameplay/capabilities.md +68 -0
  250. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  251. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  252. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  253. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  254. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  255. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  256. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  257. package/skills/jgengine-gameplay/reference-systems.md +119 -0
  258. package/skills/jgengine-world/SKILL.md +12 -3
  259. package/skills/jgengine-world/api.md +201 -58
  260. package/skills/jgengine-world/capabilities.md +116 -3
  261. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  262. package/skills/jgengine-world/recipes/controls.md +4 -0
  263. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  264. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  265. package/skills/jgengine-world/reference.md +167 -5
  266. package/skills/jgengine-world/visibility.md +28 -1
@@ -0,0 +1,162 @@
1
+ /** Physical surface families; these describe appearance, never geometry or simulation. */
2
+ export type MaterialFamily = "standard" | "fabric" | "hair" | "glass" | "metal" | "skin" | "stone" | "plastic";
3
+ /** Renderer features an authored material explicitly requires. */
4
+ export type MaterialCapability = "pbr" | "sheen" | "anisotropy" | "clearcoat" | "specular" | "transmission" | "volume" | "iridescence" | "alpha";
5
+ /** Sparse surface edits. Omitted values preserve the imported material. Angles are radians. */
6
+ export interface MaterialSurfaceParameters {
7
+ color?: string;
8
+ metalness?: number;
9
+ roughness?: number;
10
+ emissive?: string;
11
+ emissiveIntensity?: number;
12
+ normalScale?: [number, number];
13
+ alphaMode?: "opaque" | "mask" | "blend";
14
+ alphaCutoff?: number;
15
+ opacity?: number;
16
+ doubleSided?: boolean;
17
+ sheen?: number;
18
+ sheenColor?: string;
19
+ sheenRoughness?: number;
20
+ anisotropy?: number;
21
+ anisotropyRotation?: number;
22
+ clearcoat?: number;
23
+ clearcoatRoughness?: number;
24
+ ior?: number;
25
+ specularIntensity?: number;
26
+ specularColor?: string;
27
+ transmission?: number;
28
+ thickness?: number;
29
+ attenuationColor?: string;
30
+ /** Positive metres. Omit for no volume attenuation; Infinity is not serializable. */
31
+ attenuationDistance?: number;
32
+ iridescence?: number;
33
+ iridescenceIOR?: number;
34
+ iridescenceThicknessRange?: [number, number];
35
+ }
36
+ /** Supported texture semantics; packed channels follow glTF and Three's PBR shaders. */
37
+ export type MaterialTextureRole = "color" | "normal" | "roughness" | "metalness" | "ao" | "emissive" | "alpha" | "height" | "sheenColor" | "sheenRoughness" | "anisotropy" | "clearcoat" | "clearcoatRoughness" | "clearcoatNormal" | "specularIntensity" | "specularColor" | "transmission" | "thickness" | "iridescence" | "iridescenceThickness";
38
+ /** Texture channels used by the pinned standard/physical shader path. */
39
+ export type MaterialTextureChannel = "r" | "g" | "b" | "a" | "rgb" | "rgba";
40
+ /** Portable material or image authorship. */
41
+ export interface MaterialProvenance {
42
+ author: string;
43
+ license: string;
44
+ sourceUrl?: string;
45
+ description?: string;
46
+ }
47
+ /** Serializable sampling and UV metadata; metres are descriptive, UV transforms drive rendering. */
48
+ export interface MaterialTextureMetadata {
49
+ url: string;
50
+ colorSpace: "srgb" | "linear";
51
+ channel?: MaterialTextureChannel;
52
+ uvSet?: 0 | 1 | 2 | 3;
53
+ transform?: {
54
+ offset?: [number, number];
55
+ scale?: [number, number];
56
+ rotation?: number;
57
+ };
58
+ physicalSize?: [number, number];
59
+ normalConvention?: "opengl" | "directx";
60
+ sampler?: {
61
+ wrapS?: "repeat" | "clamp" | "mirror";
62
+ wrapT?: "repeat" | "clamp" | "mirror";
63
+ minFilter?: "nearest" | "linear" | "nearest-mipmap-nearest" | "nearest-mipmap-linear" | "linear-mipmap-nearest" | "linear-mipmap-linear";
64
+ magFilter?: "nearest" | "linear";
65
+ anisotropy?: number;
66
+ };
67
+ compression?: "none" | "ktx2" | "basisu" | "webp";
68
+ provenance?: MaterialProvenance;
69
+ }
70
+ /** Shared, versioned authoring data. IDs survive rename, save and export. */
71
+ export interface MaterialAsset {
72
+ schemaVersion: 1;
73
+ id: string;
74
+ name: string;
75
+ family: MaterialFamily;
76
+ capabilities: readonly MaterialCapability[];
77
+ provenance?: MaterialProvenance;
78
+ surface: MaterialSurfaceParameters;
79
+ textures?: Partial<Record<MaterialTextureRole, MaterialTextureMetadata>>;
80
+ fabric?: FabricAppearance;
81
+ hair?: HairAppearance;
82
+ }
83
+ /** Procedural cloth surface response; this does not create or bend cloth geometry. */
84
+ export interface FabricAppearance {
85
+ construction: "woven" | "fuzzy";
86
+ weaveDirection?: number;
87
+ threadScale?: number;
88
+ normalStrength?: number;
89
+ variation?: number;
90
+ }
91
+ /** Anisotropic surface approximation for existing hair cards or strand meshes, without grooming. */
92
+ export interface HairAppearance {
93
+ geometry: "cards" | "strands";
94
+ /** Radians from UV U; cards generated by hairCards use V for strand length (PI/2). */
95
+ strandDirection?: number;
96
+ /** Thin-sheet direct-light approximation on cards, independent of physical transmission. */
97
+ backlightStrength?: number;
98
+ backlightColor?: string;
99
+ }
100
+ /** Names constrain mesh and imported material slot independently; all provided tests must match. */
101
+ export interface MaterialSelector {
102
+ mesh?: string;
103
+ slot?: string;
104
+ /** Index in this mesh's material array, independent of the glTF global material index. */
105
+ slotIndex?: number;
106
+ }
107
+ /** A reusable asset reference with an optional instance-only sparse edit. */
108
+ export interface MaterialAssignment {
109
+ materialId: string;
110
+ selector: MaterialSelector;
111
+ overrides?: MaterialSurfaceParameters;
112
+ }
113
+ /**
114
+ * Exact name/index selector matching shared by authoring and rendering.
115
+ * @capability material-slot-selection match named meshes and imported material slots independently
116
+ */
117
+ export declare function matchesMaterialSelector(selector: MaterialSelector, mesh: string, slot: string, slotIndex: number): boolean;
118
+ /** Shader channel defaults; standalone alpha maps use green, base-color alpha stays in its color map. */
119
+ export declare const MATERIAL_TEXTURE_SEMANTICS: Readonly<Record<MaterialTextureRole, {
120
+ colorSpace: "srgb" | "linear";
121
+ channel: MaterialTextureChannel;
122
+ }>>;
123
+ /** An actionable authoring/import diagnostic. */
124
+ export interface MaterialDiagnostic {
125
+ severity: "error" | "warning";
126
+ code: string;
127
+ path: string;
128
+ message: string;
129
+ }
130
+ /** Feature declarations required by each sparse physical parameter. */
131
+ export declare const MATERIAL_SURFACE_CAPABILITIES: Partial<Record<keyof MaterialSurfaceParameters, MaterialCapability>>;
132
+ /** Feature declarations required by authored texture roles. */
133
+ export declare const MATERIAL_TEXTURE_CAPABILITIES: Readonly<Partial<Record<MaterialTextureRole, MaterialCapability>>>;
134
+ /** Determine feature requirements from authored fields, including intentional zero values. */
135
+ export declare function materialCapabilitiesForSurface(surface: MaterialSurfaceParameters): MaterialCapability[];
136
+ /**
137
+ * Determine required shader features from surface fields, maps and appearance adapters without editing them.
138
+ * @capability material-feature-requirements Declare material shader requirements when authors add maps or appearance adapters.
139
+ */
140
+ export declare function materialCapabilitiesForAsset(asset: Pick<MaterialAsset, "surface" | "textures" | "fabric" | "hair">): MaterialCapability[];
141
+ /**
142
+ * Validate sparse surface edits without mutating them or guessing unsupported fields.
143
+ * @capability material-surface-validation diagnose unsupported or physically invalid sparse surface edits
144
+ */
145
+ export declare function validateMaterialSurface(value: unknown, path?: string): MaterialDiagnostic[];
146
+ /** Validate material JSON and renderer prerequisites before saving or assigning it. */
147
+ export declare function validateMaterialAsset(value: unknown): MaterialDiagnostic[];
148
+ /** Parse assignment JSON at editor/RPC boundaries. Invalid data fails with an actionable path. */
149
+ export declare function parseMaterialAssignments(value: unknown): MaterialAssignment[];
150
+ /** Optional renderer limits; ordinary model assignment accepts every supported texture role. */
151
+ export interface MaterialAssignmentValidationOptions {
152
+ disallowedTextureRoles?: readonly MaterialTextureRole[];
153
+ }
154
+ /**
155
+ * Validate references and selectors without accepting accidental whole-model assignment.
156
+ * @capability material-slot-selection validate reusable asset references and explicit per-slot selectors
157
+ */
158
+ export declare function validateMaterialAssignments(assignments: readonly MaterialAssignment[], assets: readonly MaterialAsset[], options?: MaterialAssignmentValidationOptions): MaterialDiagnostic[];
159
+ /** Editable engine-authored surface starting points, independent of any game's palette or assets. */
160
+ export type MaterialTemplate = "wool" | "cotton" | "silk" | "brushed-metal" | "glass" | "coated-plastic" | "stone" | "hair-cards";
161
+ /** Create fresh, serializable material data with provenance; templates create no textures or geometry. */
162
+ export declare function createMaterialTemplate(template: MaterialTemplate, id: string, name?: MaterialTemplate): MaterialAsset;
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Exact name/index selector matching shared by authoring and rendering.
3
+ * @capability material-slot-selection match named meshes and imported material slots independently
4
+ */
5
+ export function matchesMaterialSelector(selector, mesh, slot, slotIndex) {
6
+ return (selector.mesh === undefined || selector.mesh === mesh)
7
+ && (selector.slot === undefined || selector.slot === slot)
8
+ && (selector.slotIndex === undefined || selector.slotIndex === slotIndex);
9
+ }
10
+ /** Shader channel defaults; standalone alpha maps use green, base-color alpha stays in its color map. */
11
+ export const MATERIAL_TEXTURE_SEMANTICS = {
12
+ color: { colorSpace: "srgb", channel: "rgba" },
13
+ normal: { colorSpace: "linear", channel: "rgb" },
14
+ roughness: { colorSpace: "linear", channel: "g" },
15
+ metalness: { colorSpace: "linear", channel: "b" },
16
+ ao: { colorSpace: "linear", channel: "r" },
17
+ emissive: { colorSpace: "srgb", channel: "rgb" },
18
+ alpha: { colorSpace: "linear", channel: "g" },
19
+ height: { colorSpace: "linear", channel: "r" },
20
+ sheenColor: { colorSpace: "srgb", channel: "rgb" },
21
+ sheenRoughness: { colorSpace: "linear", channel: "a" },
22
+ anisotropy: { colorSpace: "linear", channel: "rgb" },
23
+ clearcoat: { colorSpace: "linear", channel: "r" },
24
+ clearcoatRoughness: { colorSpace: "linear", channel: "g" },
25
+ clearcoatNormal: { colorSpace: "linear", channel: "rgb" },
26
+ specularIntensity: { colorSpace: "linear", channel: "a" },
27
+ specularColor: { colorSpace: "srgb", channel: "rgb" },
28
+ transmission: { colorSpace: "linear", channel: "r" },
29
+ thickness: { colorSpace: "linear", channel: "g" },
30
+ iridescence: { colorSpace: "linear", channel: "r" },
31
+ iridescenceThickness: { colorSpace: "linear", channel: "g" },
32
+ };
33
+ const FAMILIES = ["standard", "fabric", "hair", "glass", "metal", "skin", "stone", "plastic"];
34
+ const CAPABILITIES = ["pbr", "sheen", "anisotropy", "clearcoat", "specular", "transmission", "volume", "iridescence", "alpha"];
35
+ /** Feature declarations required by each sparse physical parameter. */
36
+ export const MATERIAL_SURFACE_CAPABILITIES = {
37
+ sheen: "sheen", sheenColor: "sheen", sheenRoughness: "sheen", anisotropy: "anisotropy", anisotropyRotation: "anisotropy",
38
+ clearcoat: "clearcoat", clearcoatRoughness: "clearcoat", ior: "specular", specularIntensity: "specular", specularColor: "specular",
39
+ transmission: "transmission", thickness: "volume", attenuationColor: "volume", attenuationDistance: "volume",
40
+ iridescence: "iridescence", iridescenceIOR: "iridescence", iridescenceThicknessRange: "iridescence",
41
+ alphaMode: "alpha", alphaCutoff: "alpha", opacity: "alpha",
42
+ };
43
+ /** Feature declarations required by authored texture roles. */
44
+ export const MATERIAL_TEXTURE_CAPABILITIES = Object.fromEntries(Object.keys(MATERIAL_TEXTURE_SEMANTICS).flatMap((role) => {
45
+ const capability = MATERIAL_SURFACE_CAPABILITIES[role]
46
+ ?? (role === "clearcoatNormal" ? "clearcoat" : role === "iridescenceThickness" ? "iridescence" : role === "alpha" ? "alpha" : undefined);
47
+ return capability === undefined ? [] : [[role, capability]];
48
+ }));
49
+ const FABRIC_CAPABILITIES = { fuzzy: ["sheen"], woven: ["sheen", "anisotropy"] };
50
+ const HAIR_CAPABILITIES = { cards: ["anisotropy", "alpha"], strands: ["anisotropy"] };
51
+ /** Determine feature requirements from authored fields, including intentional zero values. */
52
+ export function materialCapabilitiesForSurface(surface) {
53
+ const capabilities = new Set(["pbr"]);
54
+ for (const key of Object.keys(surface)) {
55
+ const capability = MATERIAL_SURFACE_CAPABILITIES[key];
56
+ if (capability !== undefined && surface[key] !== undefined)
57
+ capabilities.add(capability);
58
+ }
59
+ return [...capabilities];
60
+ }
61
+ /**
62
+ * Determine required shader features from surface fields, maps and appearance adapters without editing them.
63
+ * @capability material-feature-requirements Declare material shader requirements when authors add maps or appearance adapters.
64
+ */
65
+ export function materialCapabilitiesForAsset(asset) {
66
+ const capabilities = new Set(materialCapabilitiesForSurface(asset.surface));
67
+ for (const role of Object.keys(asset.textures ?? {})) {
68
+ const capability = MATERIAL_TEXTURE_CAPABILITIES[role];
69
+ if (asset.textures?.[role] !== undefined && capability !== undefined)
70
+ capabilities.add(capability);
71
+ }
72
+ if (asset.fabric)
73
+ for (const capability of FABRIC_CAPABILITIES[asset.fabric.construction])
74
+ capabilities.add(capability);
75
+ if (asset.hair)
76
+ for (const capability of HAIR_CAPABILITIES[asset.hair.geometry])
77
+ capabilities.add(capability);
78
+ return [...capabilities];
79
+ }
80
+ const UNIT_FIELDS = ["metalness", "roughness", "alphaCutoff", "opacity", "sheen", "sheenRoughness", "anisotropy", "clearcoat", "clearcoatRoughness", "specularIntensity", "transmission", "iridescence"];
81
+ const COLOR_FIELDS = ["color", "emissive", "sheenColor", "specularColor", "attenuationColor"];
82
+ const NONNEGATIVE_FIELDS = ["emissiveIntensity", "thickness"];
83
+ const SUPPORTED_FIELDS = new Set([...UNIT_FIELDS, ...COLOR_FIELDS, ...NONNEGATIVE_FIELDS, "normalScale", "alphaMode", "doubleSided", "anisotropyRotation", "ior", "attenuationDistance", "iridescenceIOR", "iridescenceThicknessRange"]);
84
+ function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
85
+ function finite(value) { return typeof value === "number" && Number.isFinite(value); }
86
+ function pair(value) { return Array.isArray(value) && value.length === 2 && value.every(finite); }
87
+ function validateProvenance(value, path) {
88
+ if (!record(value) || typeof value.author !== "string" || value.author.trim().length === 0 || typeof value.license !== "string" || value.license.trim().length === 0)
89
+ return [{ severity: "error", code: "invalid-provenance", path, message: "Expected author and license attribution." }];
90
+ if (Object.keys(value).some((key) => !["author", "license", "sourceUrl", "description"].includes(key)) || (value.description !== undefined && typeof value.description !== "string"))
91
+ return [{ severity: "error", code: "invalid-provenance", path, message: "Unsupported or invalid attribution field." }];
92
+ if (value.sourceUrl !== undefined && (typeof value.sourceUrl !== "string" || !/^https?:\/\//i.test(value.sourceUrl)))
93
+ return [{ severity: "error", code: "invalid-provenance", path: `${path}.sourceUrl`, message: "Expected an HTTP source URL." }];
94
+ return [];
95
+ }
96
+ /**
97
+ * Validate sparse surface edits without mutating them or guessing unsupported fields.
98
+ * @capability material-surface-validation diagnose unsupported or physically invalid sparse surface edits
99
+ */
100
+ export function validateMaterialSurface(value, path = "surface") {
101
+ const errors = [];
102
+ const error = (key, message) => errors.push({ severity: "error", code: "invalid-surface", path: `${path}.${key}`, message });
103
+ if (!record(value))
104
+ return [{ severity: "error", code: "invalid-surface", path, message: "Expected a surface object." }];
105
+ for (const [key, field] of Object.entries(value)) {
106
+ if (!SUPPORTED_FIELDS.has(key))
107
+ error(key, "Unsupported surface parameter.");
108
+ else if (UNIT_FIELDS.includes(key) && (!finite(field) || field < 0 || field > 1))
109
+ error(key, "Expected a finite value from 0 to 1.");
110
+ else if (COLOR_FIELDS.includes(key) && (typeof field !== "string" || !/^#[0-9a-f]{6}$/i.test(field)))
111
+ error(key, "Expected an sRGB #rrggbb color.");
112
+ else if (NONNEGATIVE_FIELDS.includes(key) && (!finite(field) || field < 0))
113
+ error(key, "Expected a finite nonnegative value.");
114
+ else if (key === "alphaMode" && !["opaque", "mask", "blend"].includes(field))
115
+ error(key, "Expected opaque, mask or blend.");
116
+ else if (key === "doubleSided" && typeof field !== "boolean")
117
+ error(key, "Expected a boolean.");
118
+ else if (key === "anisotropyRotation" && !finite(field))
119
+ error(key, "Expected a finite angle in radians.");
120
+ else if ((key === "ior" || key === "iridescenceIOR") && (!finite(field) || field < 1 || field > 2.333))
121
+ error(key, "Expected IOR from 1 to 2.333.");
122
+ else if (key === "attenuationDistance" && (!finite(field) || field <= 0))
123
+ error(key, "Expected a finite positive distance in metres.");
124
+ else if (key === "normalScale" && !pair(field))
125
+ error(key, "Expected two finite normal strengths.");
126
+ else if (key === "iridescenceThicknessRange" && (!pair(field) || field[0] < 0 || field[1] < field[0]))
127
+ error(key, "Expected an ordered nonnegative nanometre range.");
128
+ }
129
+ return errors;
130
+ }
131
+ /** Validate material JSON and renderer prerequisites before saving or assigning it. */
132
+ export function validateMaterialAsset(value) {
133
+ const errors = [];
134
+ const error = (path, message, code = "invalid-material") => errors.push({ severity: "error", code, path, message });
135
+ if (!record(value))
136
+ return [{ severity: "error", code: "invalid-material", path: "", message: "Expected a material object." }];
137
+ if (value.schemaVersion !== 1)
138
+ error("schemaVersion", "Expected material schema version 1.");
139
+ for (const key of Object.keys(value))
140
+ if (!["schemaVersion", "id", "name", "family", "capabilities", "provenance", "surface", "textures", "fabric", "hair"].includes(key))
141
+ error(key, "Unsupported material asset metadata.");
142
+ if (typeof value.id !== "string" || value.id.trim().length === 0)
143
+ error("id", "Expected a stable nonempty material ID.");
144
+ if (typeof value.name !== "string" || value.name.trim().length === 0)
145
+ error("name", "Expected a material name.");
146
+ if (!FAMILIES.includes(value.family))
147
+ error("family", "Unknown material family.");
148
+ if (value.provenance !== undefined)
149
+ errors.push(...validateProvenance(value.provenance, "provenance"));
150
+ const capabilities = Array.isArray(value.capabilities) ? value.capabilities : [];
151
+ if (!Array.isArray(value.capabilities) || capabilities.some((item) => !CAPABILITIES.includes(item)))
152
+ error("capabilities", "Expected an explicit list of supported material capabilities.");
153
+ errors.push(...validateMaterialSurface(value.surface));
154
+ if (record(value.surface)) {
155
+ for (const [field, capability] of Object.entries(MATERIAL_SURFACE_CAPABILITIES)) {
156
+ if (value.surface[field] !== undefined && !capabilities.includes(capability))
157
+ error(`surface.${field}`, `Declare the ${capability} capability.`, "missing-capability");
158
+ }
159
+ if (typeof value.surface.transmission === "number" && value.surface.transmission > 0 && value.surface.alphaMode === "blend")
160
+ error("surface.alphaMode", "Use opaque or mask coverage with transmission; alpha blending and physical transmission are distinct paths.", "unsupported-combination");
161
+ }
162
+ if (value.fabric !== undefined) {
163
+ if (!record(value.fabric) || !["woven", "fuzzy"].includes(value.fabric.construction))
164
+ error("fabric", "Expected woven or fuzzy fabric construction.");
165
+ else {
166
+ for (const [field, item] of Object.entries(value.fabric)) {
167
+ if (field === "construction")
168
+ continue;
169
+ if (value.fabric.construction === "fuzzy") {
170
+ error(`fabric.${field}`, "Thread controls require woven construction; use normal maps and surface normals for fuzzy cloth detail.");
171
+ continue;
172
+ }
173
+ if (field === "weaveDirection" ? !finite(item) : field === "threadScale" ? !finite(item) || item <= 0 : field === "normalStrength" || field === "variation" ? !finite(item) || item < 0 || item > 1 : true)
174
+ error(`fabric.${field}`, "Unsupported or invalid fabric parameter.");
175
+ }
176
+ for (const capability of FABRIC_CAPABILITIES[value.fabric.construction]) {
177
+ if (!capabilities.includes(capability))
178
+ error("fabric", `Fabric appearance requires the ${capability} capability.`, "missing-capability");
179
+ }
180
+ }
181
+ }
182
+ if (value.hair !== undefined) {
183
+ if (!record(value.hair) || !["cards", "strands"].includes(value.hair.geometry))
184
+ error("hair", "Expected cards or strands geometry.");
185
+ else
186
+ for (const [field, item] of Object.entries(value.hair)) {
187
+ if (field === "geometry")
188
+ continue;
189
+ if (field === "strandDirection" ? !finite(item) : field === "backlightStrength" ? !finite(item) || item < 0 || item > 1 : field === "backlightColor" ? typeof item !== "string" || !/^#[0-9a-f]{6}$/i.test(item) : true)
190
+ error(`hair.${field}`, "Unsupported or invalid hair parameter.");
191
+ if (value.hair.geometry !== "cards" && (field === "backlightStrength" || field === "backlightColor"))
192
+ error(`hair.${field}`, "Thin-sheet backlighting supports hair cards only.");
193
+ }
194
+ if (record(value.hair) && ["cards", "strands"].includes(value.hair.geometry)) {
195
+ for (const capability of HAIR_CAPABILITIES[value.hair.geometry]) {
196
+ if (!capabilities.includes(capability))
197
+ error("hair", `Hair approximation requires the ${capability} capability.`, "missing-capability");
198
+ }
199
+ }
200
+ }
201
+ if (value.textures !== undefined) {
202
+ if (!record(value.textures))
203
+ error("textures", "Expected a texture-role object.");
204
+ else
205
+ for (const [role, texture] of Object.entries(value.textures)) {
206
+ const semantics = MATERIAL_TEXTURE_SEMANTICS[role];
207
+ const path = `textures.${role}`;
208
+ if (semantics === undefined) {
209
+ error(path, "Unsupported texture role.");
210
+ continue;
211
+ }
212
+ if (!record(texture)) {
213
+ error(path, "Expected texture metadata.");
214
+ continue;
215
+ }
216
+ for (const field of Object.keys(texture))
217
+ if (!["url", "colorSpace", "channel", "uvSet", "transform", "physicalSize", "normalConvention", "sampler", "compression", "provenance"].includes(field))
218
+ error(`${path}.${field}`, "Unsupported texture metadata.");
219
+ if (texture.provenance !== undefined)
220
+ errors.push(...validateProvenance(texture.provenance, `${path}.provenance`));
221
+ const feature = MATERIAL_TEXTURE_CAPABILITIES[role];
222
+ if (feature !== undefined && !capabilities.includes(feature))
223
+ error(path, `Declare the ${feature} capability.`, "missing-capability");
224
+ if (typeof texture.url !== "string" || texture.url.trim().length === 0 || /^\s*(?:javascript|file):/i.test(texture.url))
225
+ error(`${path}.url`, "Expected a runtime asset URL.");
226
+ if (texture.colorSpace !== semantics.colorSpace)
227
+ error(`${path}.colorSpace`, `The ${role} role requires ${semantics.colorSpace} color space.`, "texture-color-space");
228
+ if (texture.channel !== undefined && texture.channel !== semantics.channel && !(role === "color" && texture.channel === "rgb"))
229
+ error(`${path}.channel`, `The shader samples ${semantics.channel} for ${role}; repack this texture.`, "texture-channel");
230
+ if (texture.uvSet !== undefined && ![0, 1, 2, 3].includes(texture.uvSet))
231
+ error(`${path}.uvSet`, "Expected UV set 0, 1, 2 or 3.");
232
+ if (texture.physicalSize !== undefined && (!pair(texture.physicalSize) || texture.physicalSize.some((n) => n <= 0)))
233
+ error(`${path}.physicalSize`, "Expected positive metre dimensions.");
234
+ if (texture.normalConvention !== undefined && (!["opengl", "directx"].includes(texture.normalConvention) || !["normal", "clearcoatNormal"].includes(role)))
235
+ error(`${path}.normalConvention`, "Normal conventions apply only to normal textures.");
236
+ if (texture.compression !== undefined && !["none", "ktx2", "basisu", "webp"].includes(texture.compression))
237
+ error(`${path}.compression`, "Unknown texture compression.");
238
+ if (texture.transform !== undefined) {
239
+ if (!record(texture.transform))
240
+ error(`${path}.transform`, "Expected a UV transform object.");
241
+ else
242
+ for (const [field, item] of Object.entries(texture.transform)) {
243
+ if ((field === "offset" || field === "scale") ? !pair(item) : field === "rotation" ? !finite(item) : true)
244
+ error(`${path}.transform.${field}`, "Unsupported or invalid UV transform.");
245
+ }
246
+ }
247
+ if (texture.sampler !== undefined) {
248
+ if (!record(texture.sampler))
249
+ error(`${path}.sampler`, "Expected a sampler object.");
250
+ else
251
+ for (const [field, item] of Object.entries(texture.sampler)) {
252
+ const valid = field === "wrapS" || field === "wrapT" ? ["repeat", "clamp", "mirror"].includes(item)
253
+ : field === "minFilter" ? ["nearest", "linear", "nearest-mipmap-nearest", "nearest-mipmap-linear", "linear-mipmap-nearest", "linear-mipmap-linear"].includes(item)
254
+ : field === "magFilter" ? ["nearest", "linear"].includes(item)
255
+ : field === "anisotropy" && finite(item) && item >= 1;
256
+ if (!valid)
257
+ error(`${path}.sampler.${field}`, "Unsupported or invalid sampler setting.");
258
+ }
259
+ }
260
+ }
261
+ }
262
+ if (value.family === "hair")
263
+ errors.push({ severity: "warning", code: "hair-approximation", path: "family", message: "Anisotropy and card backlighting approximate hair surfaces; this material creates no groom, strand multiscattering or simulation." });
264
+ if (record(value.surface) && typeof value.surface.anisotropy === "number" && value.surface.anisotropy > 0)
265
+ errors.push({ severity: "warning", code: "direction-prerequisite", path: "surface.anisotropy", message: "Directional highlights require suitable UVs or mesh tangents; verify their orientation on the imported mesh." });
266
+ return errors;
267
+ }
268
+ /** Parse assignment JSON at editor/RPC boundaries. Invalid data fails with an actionable path. */
269
+ export function parseMaterialAssignments(value) {
270
+ if (!Array.isArray(value))
271
+ throw new TypeError("materialAssignments: expected an array.");
272
+ return value.map((item, index) => {
273
+ const path = `materialAssignments.${index}`;
274
+ if (!record(item) || typeof item.materialId !== "string" || item.materialId.trim().length === 0)
275
+ throw new TypeError(`${path}.materialId: expected a stable material ID.`);
276
+ if (Object.keys(item).some((key) => !["materialId", "selector", "overrides"].includes(key)))
277
+ throw new TypeError(`${path}: unsupported assignment field.`);
278
+ if (!record(item.selector) || Object.keys(item.selector).length === 0)
279
+ throw new TypeError(`${path}.selector: choose a mesh, slot or slotIndex.`);
280
+ const selector = {};
281
+ for (const [key, field] of Object.entries(item.selector)) {
282
+ if (key === "mesh" || key === "slot") {
283
+ if (typeof field !== "string" || field.trim().length === 0)
284
+ throw new TypeError(`${path}.selector.${key}: expected a nonempty name.`);
285
+ selector[key] = field;
286
+ }
287
+ else if (key === "slotIndex" && typeof field === "number" && Number.isInteger(field) && field >= 0)
288
+ selector.slotIndex = field;
289
+ else
290
+ throw new TypeError(`${path}.selector.${key}: unsupported selector or invalid index.`);
291
+ }
292
+ const diagnostics = item.overrides === undefined ? [] : validateMaterialSurface(item.overrides, `${path}.overrides`);
293
+ if (diagnostics.length > 0)
294
+ throw new TypeError(`${diagnostics[0].path}: ${diagnostics[0].message}`);
295
+ return { materialId: item.materialId, selector, ...(item.overrides === undefined ? {} : { overrides: { ...item.overrides } }) };
296
+ });
297
+ }
298
+ /**
299
+ * Validate references and selectors without accepting accidental whole-model assignment.
300
+ * @capability material-slot-selection validate reusable asset references and explicit per-slot selectors
301
+ */
302
+ export function validateMaterialAssignments(assignments, assets, options = {}) {
303
+ const diagnostics = [];
304
+ const byId = new Map();
305
+ for (const asset of assets) {
306
+ if (byId.has(asset.id))
307
+ diagnostics.push({ severity: "error", code: "duplicate-material", path: asset.id, message: "Material IDs must be unique." });
308
+ byId.set(asset.id, asset);
309
+ }
310
+ assignments.forEach((assignment, index) => {
311
+ const path = `materialAssignments.${index}`;
312
+ if (!byId.has(assignment.materialId))
313
+ diagnostics.push({ severity: "error", code: "missing-material", path: `${path}.materialId`, message: `Material ${assignment.materialId} is not in the asset library.` });
314
+ const asset = byId.get(assignment.materialId);
315
+ for (const role of options.disallowedTextureRoles ?? [])
316
+ if (asset?.textures?.[role] !== undefined)
317
+ diagnostics.push({ severity: "error", code: "unsupported-texture-role", path: `${path}.materialId`, message: `Material ${assignment.materialId} uses ${role} maps unsupported on this rendering path.` });
318
+ const selector = assignment.selector;
319
+ if (!selector || Object.keys(selector).length === 0 || (selector.mesh === undefined && selector.slot === undefined && selector.slotIndex === undefined))
320
+ diagnostics.push({ severity: "error", code: "empty-selector", path: `${path}.selector`, message: "Choose a named mesh, material slot or slot index." });
321
+ if (selector?.slotIndex !== undefined && (!Number.isInteger(selector.slotIndex) || selector.slotIndex < 0))
322
+ diagnostics.push({ severity: "error", code: "invalid-selector", path: `${path}.selector.slotIndex`, message: "Expected a nonnegative slot index." });
323
+ if (assignment.overrides !== undefined)
324
+ diagnostics.push(...validateMaterialSurface(assignment.overrides, `${path}.overrides`));
325
+ });
326
+ return diagnostics;
327
+ }
328
+ /** Create fresh, serializable material data with provenance; templates create no textures or geometry. */
329
+ export function createMaterialTemplate(template, id, name = template) {
330
+ let family;
331
+ let surface;
332
+ let fabric;
333
+ let hair;
334
+ switch (template) {
335
+ case "wool":
336
+ family = "fabric";
337
+ surface = { roughness: 0.9, sheen: 0.8, sheenColor: "#ffffff", sheenRoughness: 0.85 };
338
+ fabric = { construction: "fuzzy" };
339
+ break;
340
+ case "cotton":
341
+ family = "fabric";
342
+ surface = { roughness: 0.8, sheen: 0.35, sheenColor: "#ffffff", sheenRoughness: 0.8, anisotropy: 0.15 };
343
+ fabric = { construction: "woven", weaveDirection: 0, threadScale: 100, normalStrength: 0.2, variation: 0.08 };
344
+ break;
345
+ case "silk":
346
+ family = "fabric";
347
+ surface = { roughness: 0.28, sheen: 0.7, sheenColor: "#ffffff", sheenRoughness: 0.3, anisotropy: 0.7 };
348
+ fabric = { construction: "woven", weaveDirection: 0, threadScale: 150, normalStrength: 0.05, variation: 0.03 };
349
+ break;
350
+ case "brushed-metal":
351
+ family = "metal";
352
+ surface = { metalness: 1, roughness: 0.3, anisotropy: 0.7, anisotropyRotation: 0 };
353
+ break;
354
+ case "glass":
355
+ family = "glass";
356
+ surface = { roughness: 0.05, metalness: 0, transmission: 1, ior: 1.5, thickness: 0.05, attenuationColor: "#ffffff", attenuationDistance: 2 };
357
+ break;
358
+ case "coated-plastic":
359
+ family = "plastic";
360
+ surface = { metalness: 0, roughness: 0.45, clearcoat: 1, clearcoatRoughness: 0.1 };
361
+ break;
362
+ case "stone":
363
+ family = "stone";
364
+ surface = { metalness: 0, roughness: 0.95 };
365
+ break;
366
+ case "hair-cards":
367
+ family = "hair";
368
+ surface = { roughness: 0.4, anisotropy: 0.8, alphaMode: "mask", alphaCutoff: 0.5, doubleSided: true };
369
+ hair = { geometry: "cards", strandDirection: Math.PI / 2, backlightStrength: 0.25 };
370
+ break;
371
+ default: throw new TypeError(`Unsupported material template: ${template}`);
372
+ }
373
+ const asset = {
374
+ schemaVersion: 1, id, name, family, surface, capabilities: materialCapabilitiesForAsset({ surface, fabric, hair }),
375
+ provenance: { author: "JGengine contributors", license: "Apache-2.0", sourceUrl: "https://github.com/Noisemaker111/jgengine", description: "Original editable parameter starting point. Geometry, textures and simulation are authored separately." },
376
+ ...(fabric === undefined ? {} : { fabric }), ...(hair === undefined ? {} : { hair }),
377
+ };
378
+ const errors = validateMaterialAsset(asset).filter((diagnostic) => diagnostic.severity === "error");
379
+ if (errors.length > 0)
380
+ throw new TypeError(`${errors[0].path}: ${errors[0].message}`);
381
+ return asset;
382
+ }
@@ -1,5 +1,5 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export declare const VERSION = "0.19.0";
2
+ export declare const VERSION = "0.22.0";
3
3
  /** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
4
4
  export interface ChangelogEntry {
5
5
  migrate: readonly string[];