@volter/blender-engine 0.1.148 → 0.1.149

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 (245) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +4 -11
  3. package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
  4. package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
  5. package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
  6. package/browser/addons/vrm/README.txt +3 -0
  7. package/browser/addons/vrm/__init__.py +41 -0
  8. package/browser/addons/vrm/blender_manifest.toml +16 -0
  9. package/browser/addons/vrm/common/__init__.py +1 -0
  10. package/browser/addons/vrm/common/animation.py +58 -0
  11. package/browser/addons/vrm/common/blender_manifest.py +82 -0
  12. package/browser/addons/vrm/common/char.py +16 -0
  13. package/browser/addons/vrm/common/convert.py +287 -0
  14. package/browser/addons/vrm/common/convert_any.py +48 -0
  15. package/browser/addons/vrm/common/debug.py +40 -0
  16. package/browser/addons/vrm/common/deep.py +162 -0
  17. package/browser/addons/vrm/common/error_dialog.py +274 -0
  18. package/browser/addons/vrm/common/fs.py +71 -0
  19. package/browser/addons/vrm/common/gl.py +20 -0
  20. package/browser/addons/vrm/common/gltf.py +1149 -0
  21. package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
  22. package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
  23. package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
  24. package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
  25. package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
  26. package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
  27. package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
  28. package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
  29. package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
  30. package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
  31. package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
  32. package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
  33. package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
  34. package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
  35. package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
  36. package/browser/addons/vrm/common/legacy_gltf.py +16 -0
  37. package/browser/addons/vrm/common/logger.py +84 -0
  38. package/browser/addons/vrm/common/mtoon0.blend +0 -0
  39. package/browser/addons/vrm/common/mtoon1.blend +0 -0
  40. package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
  41. package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
  42. package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
  43. package/browser/addons/vrm/common/ops/__init__.py +10 -0
  44. package/browser/addons/vrm/common/ops/export_scene.py +67 -0
  45. package/browser/addons/vrm/common/ops/icyp.py +53 -0
  46. package/browser/addons/vrm/common/ops/import_scene.py +57 -0
  47. package/browser/addons/vrm/common/ops/vrm.py +2380 -0
  48. package/browser/addons/vrm/common/ops/wm.py +167 -0
  49. package/browser/addons/vrm/common/preferences.py +362 -0
  50. package/browser/addons/vrm/common/progress.py +87 -0
  51. package/browser/addons/vrm/common/rotation.py +89 -0
  52. package/browser/addons/vrm/common/safe_removal.py +46 -0
  53. package/browser/addons/vrm/common/scene_watcher.py +230 -0
  54. package/browser/addons/vrm/common/shader.py +2243 -0
  55. package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
  56. package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
  57. package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
  58. package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
  59. package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
  60. package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
  61. package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
  62. package/browser/addons/vrm/common/version.py +161 -0
  63. package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
  64. package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
  65. package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
  66. package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
  67. package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
  68. package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
  69. package/browser/addons/vrm/common/workspace.py +227 -0
  70. package/browser/addons/vrm/common/writable_context.py +77 -0
  71. package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
  72. package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
  73. package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
  74. package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
  75. package/browser/addons/vrm/documentation/en-us/index.md +41 -0
  76. package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
  77. package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
  78. package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
  79. package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
  80. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
  81. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
  82. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
  83. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
  84. package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
  85. package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
  86. package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
  87. package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
  88. package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
  89. package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
  90. package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
  91. package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
  92. package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
  93. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
  94. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
  95. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
  96. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
  97. package/browser/addons/vrm/editor/__init__.py +1 -0
  98. package/browser/addons/vrm/editor/extension.py +425 -0
  99. package/browser/addons/vrm/editor/extension_accessor.py +75 -0
  100. package/browser/addons/vrm/editor/handler.py +84 -0
  101. package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
  102. package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
  103. package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
  104. package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
  105. package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
  106. package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
  107. package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
  108. package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
  109. package/browser/addons/vrm/editor/make_armature.py +709 -0
  110. package/browser/addons/vrm/editor/menu.py +147 -0
  111. package/browser/addons/vrm/editor/migration.py +297 -0
  112. package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
  113. package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
  114. package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
  115. package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
  116. package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
  117. package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
  118. package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
  119. package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
  120. package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
  121. package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
  122. package/browser/addons/vrm/editor/ops.py +471 -0
  123. package/browser/addons/vrm/editor/panel.py +315 -0
  124. package/browser/addons/vrm/editor/property_group.py +888 -0
  125. package/browser/addons/vrm/editor/search.py +682 -0
  126. package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
  127. package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
  128. package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
  129. package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
  130. package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
  131. package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
  132. package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
  133. package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
  134. package/browser/addons/vrm/editor/subscription.py +123 -0
  135. package/browser/addons/vrm/editor/t_pose.py +713 -0
  136. package/browser/addons/vrm/editor/validation.py +1271 -0
  137. package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
  138. package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
  139. package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
  140. package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
  141. package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
  142. package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
  143. package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
  144. package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
  145. package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
  146. package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
  147. package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
  148. package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
  149. package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
  150. package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
  151. package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
  152. package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
  153. package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
  154. package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
  155. package/browser/addons/vrm/exporter/__init__.py +1 -0
  156. package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
  157. package/browser/addons/vrm/exporter/export_scene.py +930 -0
  158. package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
  159. package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
  160. package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
  161. package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
  162. package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
  163. package/browser/addons/vrm/external/__init__.py +1 -0
  164. package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
  165. package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
  166. package/browser/addons/vrm/external/rigify_support.py +1 -0
  167. package/browser/addons/vrm/importer/__init__.py +1 -0
  168. package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
  169. package/browser/addons/vrm/importer/file_handler.py +34 -0
  170. package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
  171. package/browser/addons/vrm/importer/import_scene.py +646 -0
  172. package/browser/addons/vrm/importer/license_validation.py +198 -0
  173. package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
  174. package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
  175. package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
  176. package/browser/addons/vrm/importer/vrm_diff.py +316 -0
  177. package/browser/addons/vrm/locale/__init__.py +1 -0
  178. package/browser/addons/vrm/locale/ja_jp.py +826 -0
  179. package/browser/addons/vrm/locale/ruff.toml +8 -0
  180. package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
  181. package/browser/addons/vrm/locale/zh_hans.py +928 -0
  182. package/browser/addons/vrm/registration.py +670 -0
  183. package/browser/addons/vrm.json +18 -0
  184. package/browser/blender-emscripten-engine.mts +60 -128
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +35 -81
  187. package/browser/describe-thrown.mts +3 -6
  188. package/browser/document-chunks.mts +4 -16
  189. package/browser/gltf_export.py +3161 -0
  190. package/browser/heap-growth.mts +23 -0
  191. package/browser/index.ts +2 -0
  192. package/browser/protocol.ts +119 -35
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +197 -82
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1338 -1427
  199. package/browser/three/attach-presenter.ts +11 -39
  200. package/browser/three/blender-base64.ts +1 -36
  201. package/browser/three/blender-corner-normals.ts +1 -23
  202. package/browser/three/blender-display-shaders.json +1 -1
  203. package/browser/three/blender-display-shaders.lut +0 -0
  204. package/browser/three/blender-display-transform.ts +2 -1
  205. package/browser/three/blender-frame-columns.ts +5 -13
  206. package/browser/three/blender-graph-material.ts +18 -3
  207. package/browser/three/blender-node-graph.ts +10 -34
  208. package/browser/three/blender-physical-material.ts +2 -31
  209. package/browser/three/blender-runtime-armature.ts +7 -60
  210. package/browser/three/blender-runtime-frame.ts +9 -23
  211. package/browser/three/blender-runtime-geometry.ts +22 -26
  212. package/browser/three/blender-runtime-lighting.ts +48 -178
  213. package/browser/three/blender-runtime-shadows.ts +187 -1
  214. package/browser/three/blender-runtime-skeleton.ts +53 -1
  215. package/browser/three/blender-runtime-view.ts +505 -283
  216. package/browser/three/blender-runtime-weights.ts +9 -72
  217. package/browser/three/blender-sky.ts +13 -33
  218. package/browser/three/blender-triangulate.ts +7 -35
  219. package/browser/three/blender-viewport-sampling.ts +235 -0
  220. package/browser/three/blender-workbench-material.ts +25 -29
  221. package/browser/three/blender-workbench-outline.ts +199 -0
  222. package/browser/three/blender-world-volume.ts +62 -10
  223. package/browser/three/presenter.ts +10 -26
  224. package/browser/three/release.ts +5 -21
  225. package/browser/three/sky-precompute-worker.ts +1 -18
  226. package/browser/three/sky-worker.ts +5 -23
  227. package/browser/three/world-field-sampler.ts +12 -26
  228. package/browser/volter_three.py +9 -105
  229. package/browser/worker.ts +156 -151
  230. package/package.json +5 -2
  231. package/wasm/BUNDLE.json +17 -17
  232. package/wasm/DEPENDENCY-LICENSES.txt +14 -0
  233. package/wasm/blender_browser.data.br +0 -0
  234. package/wasm/blender_browser.js +1 -1
  235. package/wasm/blender_browser.wasm.br +0 -0
  236. package/browser/frame-transfer.mts +0 -19
  237. package/browser/three/agx-base-srgb.lut +0 -0
  238. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  239. package/browser/three/agx-look-punchy.lut +0 -0
  240. package/browser/three/blender-agx.ts +0 -235
  241. package/browser/three/blender-display-lut.ts +0 -145
  242. package/browser/three/blender-filmic.ts +0 -49
  243. package/browser/three/blender-standard.ts +0 -68
  244. package/browser/three/blender-world-visibility.ts +0 -278
  245. package/browser/three/filmic-srgb.lut +0 -0
@@ -13,7 +13,8 @@ export interface BlenderDisplaySettings {
13
13
 
14
14
  export function blenderDisplaySettingsForRender(render: Pick<RenderRequest, 'toneMapping' | 'look' | 'exposure' | 'gamma'>): BlenderDisplaySettings {
15
15
  return {
16
- transform: {none: 'Standard', filmic: 'Filmic', agx: 'AgX', neutral: 'Khronos PBR Neutral'}[render.toneMapping],
16
+ transform: {none: 'Standard', filmic: 'Filmic', agx: 'AgX', neutral: 'Khronos PBR Neutral',
17
+ aces13: 'ACES 1.3', aces20: 'ACES 2.0'}[render.toneMapping],
17
18
  look: render.look ?? 'None', exposure: render.exposure, gamma: render.gamma ?? 1,
18
19
  };
19
20
  }
@@ -1,18 +1,10 @@
1
1
  /**
2
- * THE RUNTIME FRAME'S COLUMN CONTRACT — the typed arrays a `runtime_present`
3
- * ships and `drawArraysFromColumns` draws from.
2
+ * Typed columns transferred in a Blender frame and drawn by `drawArraysFromColumns`.
4
3
  *
5
- * WHERE IT CAME FROM, and why it lives here now. The columns are Blender's
6
- * own export arena (`@volter/blender-engine`'s `browser/session-frame.mts`, `columnsToTypedArrays`):
7
- * `{offset, length, dtype, count, stride}` descriptors into one transferable
8
- * buffer, decoded on this side into the arrays below. The presenter was typed
9
- * against the retired mesh kit's `mesh-store.ts` because that store happened to
10
- * hold the same layout — Blender's — and the two were in one package. They
11
- * are not the same contract: the kit's store is MODELING STATE it mutates
12
- * (selection, hide flags, creases, an allocator per attribute type), while
13
- * this is the READ-ONLY shape of one presented frame. This file is the second
14
- * half only, so `@volter/editor-blender` names no modeling package (WORK.md §Blender in
15
- * the tab is Blender, "The mesh kit retires", M1).
4
+ * The columns are Blender's own export arena (`@volter/blender-engine`'s
5
+ * `browser/session-frame.mts`, `columnsToTypedArrays`): `{offset, length, dtype, count, stride}`
6
+ * descriptors into one transferable buffer, decoded on this side into the arrays below. This is
7
+ * the read-only shape of one presented frame, independent of editable modeling state.
16
8
  *
17
9
  * Layout (Blender's own): positions `co` (3 per vertex); faces as CSR —
18
10
  * `faceStart` (nf+1) into `corner` (one vertex index per loop), a loop's edge
@@ -28,7 +28,7 @@ interface Binding {
28
28
  borrowedFrom?: Binding;
29
29
  borrowedSource?: THREE.MeshPhysicalMaterial;
30
30
  objectInfoUniforms?: {color: THREE.IUniform<THREE.Vector4>; indexRandom: THREE.IUniform<THREE.Vector2>; particleRandom: THREE.IUniform<number>};
31
- palette?: {indices: Int32Array; sources: readonly THREE.MeshPhysicalMaterial[]};
31
+ palette?: true;
32
32
  }
33
33
  const bindings = new WeakMap<THREE.MeshPhysicalMaterial, Binding>();
34
34
  /** Graph structures whose program the GPU refused. A material whose graph is
@@ -88,7 +88,7 @@ export function bindPaletteGraph(material: THREE.MeshPhysicalMaterial, compiled:
88
88
  if (source[name]) values[`bp${index}_${name}`] = {value: source[name]};
89
89
  }
90
90
  }
91
- bindings.set(material, {compiled, uniforms: values, ramps: [], channels: channelsFor(compiled, geometry), palette: {indices, sources}});
91
+ bindings.set(material, {compiled, uniforms: values, ramps: [], channels: channelsFor(compiled, geometry), palette: true});
92
92
  material.needsUpdate = true;
93
93
  return true;
94
94
  }
@@ -108,6 +108,15 @@ interface Pending {
108
108
  started: boolean;
109
109
  }
110
110
  const pendings = new WeakMap<THREE.MeshPhysicalMaterial, Pending>();
111
+
112
+ /**
113
+ * For a viewport photograph, bind and compile pending graph materials on the capture draw.
114
+ * prepareDraw’s photograph flag selects this synchronous path.
115
+ */
116
+ let photographing = false;
117
+ export function photographGraphs(on: boolean): void {
118
+ photographing = on;
119
+ }
111
120
  /** The proxy material a swapped-in program was compiled through, disposed
112
121
  * once the material itself holds that program (its second draw after). */
113
122
  const retiring = new WeakMap<THREE.MeshPhysicalMaterial, {shadow: THREE.Material; draws: number}>();
@@ -324,7 +333,13 @@ export function bindGraphDraw(material: THREE.MeshPhysicalMaterial, geometry: TH
324
333
  retiring.delete(material);
325
334
  }
326
335
  const waiting = pendings.get(material);
327
- if (waiting && !waiting.started) {
336
+ if (waiting && photographing) {
337
+ pendings.delete(material);
338
+ for (const ramp of bindings.get(material)?.ramps ?? []) ramp.dispose();
339
+ waiting.binding.channels = channelsFor(waiting.binding.compiled, geometry);
340
+ bindings.set(material, waiting.binding);
341
+ material.needsUpdate = true;
342
+ } else if (waiting && !waiting.started) {
328
343
  waiting.started = true;
329
344
  waiting.binding.channels = channelsFor(waiting.binding.compiled, geometry);
330
345
  const shadow = shadowOf(material);
@@ -212,20 +212,12 @@ function prop<T>(node: GraphNode, name: string): T {
212
212
  return node.props[name] as T;
213
213
  }
214
214
 
215
- /** What EEVEE's node library reads from EEVEE itself, transcribed: the
216
- * shading globals `g_data` (`init_globals`: N faces the viewer, Ng the true
217
- * normal), the object/view matrix accessors `transform_utils` calls, the
218
- * object's negative-scale flag, `coordinate_incoming` (toward the eye; the
219
- * view axis in orthographic), `derivative_scale_get` (1 at full resolution)
220
- * and codegen's precise `dF_impl`, which the bump sub-function evaluates
221
- * with its derivative flag set (`gpu_shader_codegen_lib.glsl`).
222
- * BLENDER'S WORLD IS Z-UP: the presenter's model root maps it into three's
223
- * Y-up world by the exact permutation (x, y, z) -> (x, z, -y)
224
- * (`blender-runtime-view.ts`), so every world-space quantity handed to the
225
- * library goes back through `blender_from_three`, and a normal the graph
226
- * returns comes out through its transpose. A transmissive material's fragment shader already
227
- * declares `modelMatrix` (three's `transmission_pars_fragment`), so the prelude yields it there:
228
- * declared twice, the Stoneguard file's water failed to compile ("'modelMatrix' : redefinition"). */
215
+ /**
216
+ * Provide the EEVEE node library’s shading globals, object and view accessors, scale flags,
217
+ * incoming coordinates and derivative helpers. Transform world quantities through the model root’s
218
+ * exact Z-up to Y-up permutation before passing them to Blender’s code, and transform returned
219
+ * normals back. Yield modelMatrix when three’s transmission shader already declares it.
220
+ */
229
221
  const EEVEE_GLOBALS = `
230
222
  #define GPU_FRAGMENT_SHADER
231
223
  #define FrontFacing gl_FrontFacing
@@ -837,8 +829,7 @@ class Compiler {
837
829
  uniform, node: key, name: image.name, revision: image.revision, extension,
838
830
  closest: interpolation === 'Closest',
839
831
  mipmap: projection !== 'SPHERE' && projection !== 'TUBE',
840
- // EEVEE samples tiles only through a flat projection; any other
841
- // treats the first tile as the image (#141776).
832
+ // EEVEE samples tiles only through flat projection; other projections sample the first tile.
842
833
  tiled: projection === 'FLAT' && node.props['image_tiled'] === true,
843
834
  });
844
835
  const tiled = this.s.images.at(-1)!.tiled;
@@ -866,13 +857,7 @@ class Compiler {
866
857
  if (extension === 'CLIP')
867
858
  body.push(`if (any(lessThan(${co}.xy, vec2(0.0))) || any(greaterThan(${co}.xy, vec2(1.0)))) { ${color} = vec4(0.0); ${alpha} = 0.0; }`);
868
859
  }
869
- // node_shader_gpu_tex_image's Color output over the file's own texels (straight, or
870
- // premultiplied for a PREMUL image, as Blender's GPU texture holds them): alpha cleared for
871
- // ignored, packed or data alpha; otherwise straight when Alpha is used, so a factor does
872
- // not multiply it in twice, and premultiplied when it is not. Measured in Blender 5.2's
873
- // EEVEE: a straight red texel at alpha 0.5 emits 0.502 with Alpha unused, 1.0 with it used.
874
- // A PREMUL sRGB image differs: the sampler decodes the premultiplied bytes, `decode(r·a)`,
875
- // where Blender's texture holds `linear(r)·a`, so its partly transparent texels read darker.
860
+ // Match node_shader_gpu_tex_image alpha handling: ignore packed/data alpha; use straight colour when Alpha is linked and premultiplied colour otherwise. PREMUL sRGB decoding retains the file’s premultiplied-byte convention.
876
861
  const mode = prop<string>(node, 'image_alpha_mode');
877
862
  const alphaUsed = this.uses(key, 1);
878
863
  const c = outs([0]);
@@ -1007,16 +992,7 @@ class Compiler {
1007
992
  }
1008
993
  }
1009
994
 
1010
- /**
1011
- * ONLY THE FUNCTIONS THE GRAPH REACHES, as Blender's codegen links only the
1012
- * functions a material calls. A node's file holds every variant of it (each
1013
- * Noise type and dimension, each Voronoi feature), and a program carrying all
1014
- * of them measured ~155 KB of GLSL for a 30-line graph -- compile time the
1015
- * viewport spent blocked. Everything that is not a plain function definition
1016
- * (macros, which may generate or call functions; structs; constants) is kept,
1017
- * and a function is kept when its name appears in kept text, to a fixpoint.
1018
- * Overloads of a kept name are all kept.
1019
- */
995
+ /** Keep GLSL definitions reached from the graph to a name-reference fixpoint, retaining overloads together. Keep macros, structs and constants because they may define or call functions. */
1020
996
  function shakeLibrary(library: string, root: string): string {
1021
997
  type Chunk = {text: string; name: string | null};
1022
998
  const chunks: Chunk[] = [];
@@ -1060,12 +1036,12 @@ function shakeLibrary(library: string, root: string): string {
1060
1036
  return chunks.filter(c => keep.has(c)).map(c => c.text).join('');
1061
1037
  }
1062
1038
 
1063
- /** The varying a UV layer arrives in; `''` is the render UV. */
1064
1039
  /** The varying a graph reads Blender attribute `name` through. */
1065
1040
  export function attributeVarying(name: string): string {
1066
1041
  return `v${graphAttributeName(name)}`;
1067
1042
  }
1068
1043
 
1044
+ /** The varying a UV layer arrives in; `''` is the render UV. */
1069
1045
  export function uvVarying(name: string): string {
1070
1046
  return name === '' ? 'vBlenderUvRender' : `vBlenderUv_${[...name].map(c => c.charCodeAt(0).toString(16)).join('')}`;
1071
1047
  }
@@ -7,7 +7,6 @@ import {z} from 'zod';
7
7
  import {applyGraphShader, bindGraphDraw, bindPaletteGraph, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
8
8
  import type {CompiledGraph} from './blender-node-graph';
9
9
  import {bindNamedUvChannels} from './blender-texture-samplers';
10
- import {WORLD_VISIBILITY,copyWorldVisibility,worldVisibilityShader,worldVisibilityUniforms} from './blender-world-visibility';
11
10
 
12
11
  const scalar = z.number().finite();
13
12
  const color = z.tuple([scalar, scalar, scalar]);
@@ -56,7 +55,6 @@ function graphShadow(material: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMa
56
55
  // which `copy` replaces with the material's own (it leaves hooks alone).
57
56
  applyPhysicalMaterial(shadow);
58
57
  shadow.copy(material);
59
- copyWorldVisibility(material,shadow);
60
58
  const values = uniforms.get(shadow)!;
61
59
  if (held) {
62
60
  values.blenderCoatIor.value = held.blenderCoatIor.value;
@@ -151,7 +149,6 @@ export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.
151
149
  return null;
152
150
  }
153
151
  copy.copy(source);
154
- copyWorldVisibility(source,copy);
155
152
  const held = uniforms.get(source)!;
156
153
  const values = uniforms.get(copy)!;
157
154
  values.blenderCoatIor.value = held.blenderCoatIor.value;
@@ -163,16 +160,6 @@ export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.
163
160
  return copy;
164
161
  }
165
162
 
166
- /** A copy of a presented material that ONE object can wear and a script can recolour (Model
167
- * Play's `tint`/`setOpacity`): the same hooks, physical values, maps and private uniforms,
168
- * drawn from the material's constants -- `color`, `opacity`, `map`, `emissive` -- with no
169
- * node graph, since a graph's Base Color and Alpha replace those constants in the shader.
170
- * Null for a material that is not one of the presenter's. The caller owns and disposes it. */
171
- export function ownedSurfaceMaterial(source: THREE.Material): THREE.MeshPhysicalMaterial | null {
172
- if (!(source instanceof THREE.MeshPhysicalMaterial) || !uniforms.has(source) || !materialDrawHooksSupported(source)) return null;
173
- return graphShadow(source);
174
- }
175
-
176
163
  export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
177
164
  clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
178
165
  const data = input ?? defaults;
@@ -195,7 +182,7 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
195
182
  blenderWorldExtinction: {value: new THREE.Vector3()}};
196
183
  uniforms.set(material, values);
197
184
  const held = values;
198
- material.customProgramCacheKey = () => `blender-principled-physical-v6${WORLD_VISIBILITY ? '-atlas' : ''}${graphProgramKey(material)}`;
185
+ material.customProgramCacheKey = () => `blender-principled-physical-v6${graphProgramKey(material)}`;
199
186
  material.onBeforeRender = (renderer, scene, camera, geometry, object) => {
200
187
  bindNamedUvChannels(material, geometry);
201
188
  bindGraphDraw(material, geometry, renderer, scene, camera, object, graphShadow);
@@ -213,16 +200,6 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
213
200
  .map(i => (i <= 3 ? `#ifndef USE_UV${i}\nattribute vec2 uv${i};\n#endif` : `attribute vec2 uv${i};`))
214
201
  .join('\n') + '\n' + shader.vertexShader;
215
202
  Object.assign(shader.uniforms, held);
216
- // The sky-light atlas exists in the shader only while it is on: its three
217
- // samplers would count against every material's sixteen (WORLD_VISIBILITY).
218
- if (WORLD_VISIBILITY) {
219
- worldVisibilityUniforms(material,shader.uniforms);
220
- shader.vertexShader='varying vec3 blenderWorldPosition;\n'+shader.vertexShader;
221
- shader.vertexShader=shader.vertexShader.replace('#include <worldpos_vertex>',
222
- '#include <worldpos_vertex>\n#if defined(USE_ENVMAP) || defined(USE_SHADOWMAP) || defined(USE_TRANSMISSION)\nblenderWorldPosition=worldPosition.xyz;\n#else\nblenderWorldPosition=(modelMatrix*vec4(transformed,1.)).xyz;\n#endif');
223
- shader.fragmentShader=shader.fragmentShader.replace('#include <envmap_physical_pars_fragment>',
224
- '#include <envmap_physical_pars_fragment>\n'+worldVisibilityShader);
225
- }
226
203
  shader.fragmentShader = `uniform float blenderCoatIor;
227
204
  uniform vec3 blenderWorldExtinction;
228
205
  vec3 blenderInfiniteTransmission() { return vec3(
@@ -267,13 +244,7 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
267
244
  .replace('rectAreaLight = rectAreaLights[ i ];',
268
245
  'rectAreaLight = rectAreaLights[ i ]; rectAreaLight.color *= exp(-blenderWorldExtinction * length(rectAreaLight.position-geometryPosition));')
269
246
  .replace('getAmbientLightIrradiance( ambientLightColor )', 'getAmbientLightIrradiance( ambientLightColor ) * blenderInfiniteTransmission()');
270
- const visible = WORLD_VISIBILITY ? THREE.ShaderChunk.lights_fragment_maps
271
- .replace('getIBLIrradiance( geometryNormal )',
272
- 'blenderWorldIrradiance( geometryNormal, getIBLIrradiance( geometryNormal ) )')
273
- .replaceAll(/(getIBL(?:Radiance|AnisotropyRadiance)\([^;]+\))/g,
274
- '$1 * blenderWorldReflectionVisibility(geometryViewDir,geometryNormal,material.roughness)')
275
- : THREE.ShaderChunk.lights_fragment_maps;
276
- const environment = visible
247
+ const environment = THREE.ShaderChunk.lights_fragment_maps
277
248
  .replaceAll(/(getIBL(?:Irradiance|Radiance|AnisotropyRadiance)\([^;]+\))/g, '$1 * blenderInfiniteTransmission()');
278
249
  shader.fragmentShader = shader.fragmentShader.replace('#include <lights_physical_fragment>', physical)
279
250
  .replace('#include <lights_fragment_begin>', lighting)
@@ -1,31 +1,4 @@
1
- /**
2
- * THE ARMATURE OVERLAY — Blender's bones, drawn in three.js over the engine's
3
- * own data (ARCHITECTURE-CORE §Blender north star, "Inspection parity, not
4
- * editing parity" and "The reference is Blender's SOURCE as well as its
5
- * frames"; WORK.md §Blender in the tab is Blender, "Inspection parity", I4).
6
- *
7
- * Blender's overlay ENGINE is never run, ported or recorded: what crosses is
8
- * the data its draw functions read — per bone a pose matrix, a length, a
9
- * parent, its hide/select/active flags — and this file is our own drawing of
10
- * the same shapes with Blender's own vertex tables and Blender's own theme
11
- * colours. Every metric below cites the file and constant it came from, at the
12
- * engine's pin (Blender 5.2.0, `fbe6228777e7`).
13
- *
14
- * WHAT IS DRAWN, and what is not. `display_type` OCTAHEDRAL and STICK are
15
- * drawn (`bone_draw_octa` / `bone_draw_line`, `overlay_armature.cc:1415-1500`).
16
- * BBONE, ENVELOPE and WIRE fall back to the octahedron and say so through a
17
- * frame warning — Blender's B-Bone needs the per-segment matrices
18
- * (`draw_bone_update_disp_matrix_bbone`, `:1186`) and its envelope needs the
19
- * head/tail radii, neither of which the door carries yet. A standing named
20
- * warning, never a silent degrade.
21
- *
22
- * EDIT MODE IS NOT DRAWN EITHER, for a reason worth stating: in edit mode
23
- * Blender draws the EDIT bones (`ED_armature_ebone_to_mat4`), which are a
24
- * different set of matrices from `pose.bones`, and drawing the pose there
25
- * would be a confident picture of the wrong thing. The door reports the mode;
26
- * an armature in EDIT mode draws its pose with the object-mode colours and the
27
- * frame carries the warning.
28
- */
1
+
29
2
  import * as THREE from 'three';
30
3
  import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
31
4
  import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
@@ -51,7 +24,7 @@ export const boneSchema = z
51
24
  active: z.boolean(),
52
25
  lockedWeight: z.boolean(),
53
26
  /** `Bone.matrix_local`, the rest matrix in the armature's space. */
54
- rest: matrixSchema.optional(),
27
+ rest: matrixSchema,
55
28
  /** The channels' own values: what a channel no action animates keeps. */
56
29
  channels: z.object({
57
30
  mode: z.string(),
@@ -60,7 +33,7 @@ export const boneSchema = z
60
33
  rotation_euler: z.array(scalar).length(3),
61
34
  rotation_axis_angle: z.array(scalar).length(4),
62
35
  scale: z.array(scalar).length(3),
63
- }).strict().optional(),
36
+ }).strict(),
64
37
  /** Parenting flags that are not Blender's default, by name. */
65
38
  inherit: z.array(z.string()).optional(),
66
39
  /** The bone's constraint stack, in order. */
@@ -114,7 +87,7 @@ export const armatureAnimationSchema = z.object({
114
87
  blendType: z.string(),
115
88
  extrapolation: z.string(),
116
89
  /** The NLA is in tweak mode (`use_tweak_mode`), which the presenter does not play. */
117
- tweak: z.boolean().optional(),
90
+ tweak: z.boolean(),
118
91
  tracks: z.array(z.object({ name: z.string(), mute: z.boolean(), solo: z.boolean(), strips: z.array(nlaStripSchema) }).strict()),
119
92
  /** Bones whose channels a driver sets. */
120
93
  drivers: z.array(z.string()).optional(),
@@ -130,7 +103,7 @@ export const armatureSchema = z
130
103
  mode: z.string(),
131
104
  bones: z.array(boneSchema),
132
105
  /** `animation_data.action`, the action it plays (`session.py` `_armatures`). */
133
- action: z.string().nullable().optional(),
106
+ action: z.string().nullable(),
134
107
  animation: armatureAnimationSchema.optional(),
135
108
  })
136
109
  .strict();
@@ -265,18 +238,7 @@ function wireThickness(bone: Bone): number {
265
238
  * (`overlay_shape.cc:368-375`): a 10 CSS px bar at UI scale 1. */
266
239
  const STICK_WIDTH = 10;
267
240
 
268
- /**
269
- * The CORE of that bar, in the bone's own colour.
270
- *
271
- * Blender's stick fragment is a gradient, not two bands:
272
- * `fac = smoothstep(1.0, 0.2, color_fac)` then `mix(inner, wire, fac)`
273
- * (`overlay_armature_stick_frag.glsl:13-14`), where `color_fac` runs 1 at the
274
- * centre line to 0 at either edge. The blend passes 50% at `color_fac` 0.6,
275
- * i.e. 0.4 of the half-width — so the band that is more bone than wire is
276
- * 0.4 × 10 = 4 px. `LineMaterial` draws a flat width, so the bar is two
277
- * lines, 10 px of wire colour with a 4 px core, and that 4 is this
278
- * measurement rather than a choice.
279
- */
241
+ /** Bone-colour core spans 0.4 × STICK_WIDTH, matching the midpoint of Blender's stick-fragment gradient. */
280
242
  const STICK_CORE_WIDTH = 4;
281
243
 
282
244
  /**
@@ -300,22 +262,7 @@ const BONE_SOLID_VERTEX = /* glsl */ `
300
262
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
301
263
  }
302
264
  `;
303
- /**
304
- * THE OUTPUT TRANSFORM HAS TO BE ASKED FOR, and forgetting it is what the walk
305
- * caught: a `ShaderMaterial` writes `gl_FragColor` RAW. three's own materials
306
- * end with `#include <colorspace_fragment>`, which is what converts the linear
307
- * working value to the renderer's `outputColorSpace`; without it the theme's
308
- * `bone_solid` (#b2b2b2, linear 0.44) reached the framebuffer as 0.44 and the
309
- * bones photographed at byte 112 against Blender's 178 — visibly dark grey
310
- * where Blender's are light. Tone mapping is deliberately NOT included: an
311
- * overlay is drawn after the view transform in Blender and is not a
312
- * scene-referred value.
313
- *
314
- * ONLY THE `_fragment` HALF: three PREPENDS `colorspace_pars_fragment` to every
315
- * `ShaderMaterial` already, so including it here declared `LinearTransferOETF`,
316
- * `sRGBTransferEOTF` and `sRGBTransferOETF` twice and the whole program refused
317
- * to compile ("function already has a body", measured live in `cyclotron console`).
318
- */
265
+ /** Apply colorspace_fragment to overlay colours without tone mapping. ShaderMaterial already supplies colorspace_pars_fragment. */
319
266
  const BONE_SOLID_FRAGMENT = /* glsl */ `
320
267
  varying vec3 vColor;
321
268
  void main() {
@@ -1,6 +1,5 @@
1
- /** The runtime frame's INCREMENTAL contract — the one thing the worker that
2
- * builds a frame (`runtime_present`) and the presenter that applies it
3
- * (`blender-runtime-view.ts`) both have to agree on.
1
+ /** Incremental Blender frames transferred by the worker and applied by
2
+ * `blender-runtime-view.ts`.
4
3
  *
5
4
  * A present ships the columns of the meshes whose store revision moved since
6
5
  * the last frame that session sent, and a bare REFERENCE for the rest; the
@@ -9,31 +8,18 @@
9
8
  * a marker string.
10
9
  */
11
10
 
12
- /** A mesh the presenter is expected to already hold, named by the store and
13
- * revision it was last sent as. `store`/`revision` are diagnostic — they are
14
- * what a stale-geometry report would quote — and `unchanged` is the
15
- * discriminator. */
16
- export interface UnchangedMesh {
17
- store: number;
18
- revision: number;
19
- unchanged: true;
20
- }
21
-
22
11
  /** The presenter's refusal when a reference names geometry it does not hold.
23
12
  *
24
- * It is a MARKER IN THE MESSAGE rather than an error subclass because the
25
- * refusal crosses a `Function`-constructed page eval and an SSE channel before
26
- * the worker sees it, and only the message survives that trip. The worker
27
- * answers by re-sending the whole frame in full; a page that reloaded under a
28
- * still-running Python session is the case this exists for. */
13
+ * The message marker survives the worker response boundary. The worker
14
+ * answers by re-sending the whole frame in full, restoring geometry after
15
+ * the presenter has lost its retained frame. */
29
16
  export const UNKNOWN_GEOMETRY = 'RUNTIME_FRAME_UNKNOWN_GEOMETRY';
30
17
 
31
18
  /** The presenter's refusal when a material names a PICTURE it does not hold.
32
19
  *
33
20
  * The same contract one level up from the meshes: a frame carries an image's
34
- * bytes once per `(name, revision)` and after that its materials name it
35
- * (`blender/python/runtime_session.py`, `_stage_images`). The worker cannot
36
- * answer this one — the bytes are Python's to re-stage — so unlike
37
- * `UNKNOWN_GEOMETRY` it travels all the way back, and `RuntimeSession.present`
38
- * drops its record and presents the frame again in full. */
21
+ * bytes once per `(name, revision)` and after that its materials name it.
22
+ * The worker cannot supply Python's image bytes, so the refusal reaches
23
+ * `browser/session.py`'s `Session`, which clears its image-carry record and
24
+ * retries the present with the image data. */
39
25
  export const UNKNOWN_IMAGE = 'RUNTIME_FRAME_UNKNOWN_IMAGE';
@@ -8,12 +8,6 @@
8
8
  * crosses as transferable buffers, never as nested number arrays. The
9
9
  * boundary JSON door (`drawRuntimeGeometry`) remains for a frame that
10
10
  * arrives as JSON.
11
- *
12
- * A drawn geometry carried a `userData.authoredUv` mark until 2026-09-19.
13
- * MEASURED when this file became `@volter/editor-blender`'s: its only readers were the
14
- * kit's own `bmesh.ts` and `unwrap.ts`, and a presented frame's geometry is
15
- * never handed to the kit — it is the Model document's scene. Data with no
16
- * reader is not authored, so the mark is gone.
17
11
  */
18
12
  import * as THREE from 'three';
19
13
  import { customCornerNormals, type Normal } from './blender-corner-normals';
@@ -83,10 +77,10 @@ interface DrawView {
83
77
  sharp: Iterable<[number, number]>;
84
78
  /** A corner's UV, or undefined when the mesh has no map. */
85
79
  uv: ((corner: number) => [number, number]) | null;
86
- uvLayers?: {name: string; read: (corner: number) => [number, number]}[];
80
+ uvLayers: {name: string; read: (corner: number) => [number, number]}[];
87
81
  /** A corner's value of each generic attribute layer, by vertex, corner and
88
82
  * face; `corner` and `face` are undefined for a loose vertex. */
89
- attributeLayers?: {name: string; domain: string;
83
+ attributeLayers: {name: string; domain: string;
90
84
  read: (vertex: number, corner: number | undefined, face: number | undefined) => ArrayLike<number>}[];
91
85
  }
92
86
 
@@ -115,7 +109,7 @@ function attributeFloat4(type: string, values: ArrayLike<number>, i: number): [n
115
109
  function graphAttributeLayers(
116
110
  attributes: readonly {name: string; domain: string; type: string; data: ArrayLike<unknown>}[],
117
111
  flat: (data: ArrayLike<unknown>, type: string) => ArrayLike<number>,
118
- ): NonNullable<DrawView['attributeLayers']> {
112
+ ): DrawView['attributeLayers'] {
119
113
  return attributes
120
114
  .filter(a => GRAPH_ATTRIBUTE_TYPES.has(a.type) && a.domain !== 'EDGE' && !a.name.startsWith('.')
121
115
  && a.name !== 'position' && a.name !== 'custom_normal' && !(a.type === 'FLOAT2' && a.domain === 'CORNER'))
@@ -205,8 +199,8 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
205
199
  const positions = new Float32Array(capacity * 3),
206
200
  texcoords = new Float32Array(capacity * 2);
207
201
  const sourceVertex = new Uint32Array(capacity);
208
- const uvLayers = (view.uvLayers ?? []).map(layer => ({...layer, data: new Float32Array(capacity * 2)}));
209
- const attributeLayers = (view.attributeLayers ?? []).map(layer => ({...layer, data: new Float32Array(capacity * 4)}));
202
+ const uvLayers = view.uvLayers.map(layer => ({...layer, data: new Float32Array(capacity * 2)}));
203
+ const attributeLayers = view.attributeLayers.map(layer => ({...layer, data: new Float32Array(capacity * 4)}));
210
204
  const orco = view.orco ? new Float32Array(capacity * 3) : null;
211
205
  const indices = new Uint32Array(capacity),
212
206
  shared = new Map<string, number>();
@@ -310,15 +304,23 @@ export function geometryFromDrawArrays(
310
304
  geometry.addGroup(group.start, group.count, group.materialIndex);
311
305
  if (arrays.normals) geometry.setAttribute('normal', new THREE.BufferAttribute(arrays.normals, 3));
312
306
  else geometry.computeVertexNormals();
313
- // The draw's own vertex map (see `DrawArrays.sourceVertex`). Optional here
314
- // because a frame may arrive ALREADY DRAWN from the worker, whose buffers
315
- // predate this attribute; a reader that needs it says so by name rather than
316
- // colouring by a guessed index.
307
+ // The draw’s source-vertex map. A reader that needs the map refuses an absent attribute instead of substituting an index.
317
308
  if (arrays.sourceVertex)
318
309
  geometry.setAttribute('blenderVertex', new THREE.BufferAttribute(arrays.sourceVertex, 1));
319
310
  return geometry;
320
311
  }
321
312
 
313
+ /** Choose a mesh’s render UV, then active UV, then first UV; return -1 when none exist. Only CORNER FLOAT2 layers qualify, so stale names and generic attributes are skipped. */
314
+ export function renderMapIndex(maps: readonly { name: string }[], renderUv: string | null | undefined,
315
+ activeUv: string | null | undefined): number {
316
+ for (const name of [renderUv, activeUv]) {
317
+ if (!name) continue;
318
+ const index = maps.findIndex((map) => map.name === name);
319
+ if (index >= 0) return index;
320
+ }
321
+ return maps.length ? 0 : -1;
322
+ }
323
+
322
324
  /** Blender already supplied corner normals and triangulation. Read those
323
325
  * columns directly: no polygon triangulation, adjacency graph, vector objects
324
326
  * or stringified floating-point keys. Two passes size final buffers exactly. */
@@ -326,9 +328,7 @@ function drawNativeColumns(c: MeshColumns, hash: string): DrawArrays {
326
328
  const triangles = c.cornerTri!, normal = c.cornerNormal!;
327
329
  const nv = c.co.length / 3, nc = c.corner.length, nf = c.faceStart.length - 1;
328
330
  const maps = c.attributes.filter(a => a.type === 'FLOAT2' && a.domain === 'CORNER');
329
- const mapName = c.renderUv ?? c.activeUv ?? maps[0]?.name;
330
- const activeMap = maps.findIndex(a => a.name === mapName);
331
- if (mapName && activeMap < 0) throw new Error(`Runtime UV map ${mapName} is missing`);
331
+ const activeMap = renderMapIndex(maps, c.renderUv, c.activeUv);
332
332
  const attributes = graphAttributeLayers(c.attributes, data => data as ArrayLike<number>);
333
333
  const width = 3 + maps.length * 2 + attributes.length * 4;
334
334
  const scratch = new Float32Array(width), comparison = new Float32Array(width);
@@ -428,9 +428,7 @@ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays
428
428
  if (c.cornerTri && c.cornerNormal) return withIslandRandom(drawNativeColumns(c, hash),c.co.length/3,storedMeshEdges(c.edge));
429
429
  const nv = c.co.length / 3;
430
430
  const maps = c.attributes.filter((a) => a.type === 'FLOAT2' && a.domain === 'CORNER');
431
- const mapName = c.renderUv ?? c.activeUv ?? maps[0]?.name;
432
- const map = maps.find((a) => a.name === mapName);
433
- if (mapName && !map) throw new Error(`Runtime UV map ${mapName} is missing`);
431
+ const map = maps[renderMapIndex(maps, c.renderUv, c.activeUv)];
434
432
  const uvData = map ? (map.data as Float32Array) : null;
435
433
  const ne = c.edge.length / 2;
436
434
  const edges: [number, number][] = new Array(ne);
@@ -536,9 +534,7 @@ export function drawRuntimeGeometry(data: MeshData): THREE.BufferGeometry {
536
534
  `Runtime attribute ${attribute.name} has ${attribute.data.length} values for ${counts[attribute.domain]} ${attribute.domain} elements`,
537
535
  );
538
536
  const maps = (data.attributes ?? []).filter((a) => a.type === 'FLOAT2' && a.domain === 'CORNER');
539
- const mapName = data.render_uv ?? data.active_uv ?? maps[0]?.name;
540
- const map = maps.find((a) => a.name === mapName);
541
- if (mapName && !map) throw new Error(`Runtime UV map ${mapName} is missing`);
537
+ const map = maps[renderMapIndex(maps, data.render_uv, data.active_uv)];
542
538
  const uv = map?.data as [number, number][] | undefined;
543
539
  const view: DrawView = {
544
540
  vertexCount: vertices.length,
@@ -560,8 +556,8 @@ export function drawRuntimeGeometry(data: MeshData): THREE.BufferGeometry {
560
556
  attributeLayers: graphAttributeLayers(data.attributes ?? [],
561
557
  values => (Array.from(values) as (number | number[])[]).flat()),
562
558
  };
563
- // Native frames always supply stored edges. Legacy JSON without that
564
- // order retains its old unsupported zero input rather than guessing IDs.
559
+ // Native frames always supply stored edges. A JSON frame without that
560
+ // order keeps an unsupported zero input rather than guessing IDs.
565
561
  const arrays=drawCore(view);
566
562
  return geometryFromDrawArrays(view.edges?withIslandRandom(arrays,vertices.length,view.edges):arrays);
567
563
  }