@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
@@ -56,6 +56,9 @@ import {
56
56
  sleep,
57
57
  } from './blender-engine.mts';
58
58
  import sessionPython from './session.py?raw';
59
+ // Imported by session.py’s gltf-export operation from the same directory.
60
+ import gltfExportPython from './gltf_export.py?raw';
61
+ import { sessionAddonFiles } from './session-addons.mts';
59
62
 
60
63
  /** Blender's own resource root inside the program, spelled the same way the
61
64
  * standalone skew spells it so `BLENDER_SYSTEM_*` reads identically. */
@@ -81,17 +84,22 @@ interface BrowserFileSystemLike {
81
84
  on?(event: 'change' | 'delete', listener: (path: string) => void): unknown;
82
85
  }
83
86
 
87
+ /** A write that replaces whatever is at `path` (`BlenderFiles.writeFile`). */
88
+ function replaceFileSync(filesystem: BrowserFileSystemLike, path: string, data: string | Uint8Array): void {
89
+ if (filesystem.existsSync(path)) filesystem.unlinkSync(path);
90
+ filesystem.writeFileSync(path, data);
91
+ }
92
+
84
93
  interface WaliProgram {
85
94
  run(
86
95
  argv: readonly string[],
87
96
  options: {
88
97
  onStdout?(chunk: string): void;
89
98
  onStderr?(chunk: string): void;
90
- /** The WALI RUNTIME's own word to its host, not the program's stream --
91
- * today the one-shot notice that an unimplemented syscall was answered
92
- * `-ENOSYS` instead of trapping. Declared here because the call site
93
- * passes it (`run(...)` below) and this local shape is the only thing
94
- * that types that call. */
99
+ /**
100
+ * The WALI runtime’s diagnostic callback, separate from the program’s output. Reports an
101
+ * unimplemented syscall answered with -ENOSYS.
102
+ */
95
103
  onDiagnostic?(message: string): void;
96
104
  },
97
105
  ): Promise<{ exitCode: number; stdout: string; stderr: string }>;
@@ -118,15 +126,8 @@ async function substrate(module: string): Promise<Record<string, unknown>> {
118
126
  }
119
127
 
120
128
  /**
121
- * Blender's runtime tree — its scripts, datafiles and Python — into the
122
- * program's filesystem.
123
- *
124
- * ONE INDEX AND ONE BLOB, not 2,276 requests. The tree is 78 MB in ~2,300
125
- * files and every one of them has to exist before Blender's first act, so the
126
- * editor serves a JSON index and the bytes concatenated in its order
127
- * (`blender-wasm-artifact.ts`); this walks the index and slices. The
128
- * standalone skew's equivalent is the packed `.data` payload, and this is the
129
- * same shape for a filesystem that is not a module's.
129
+ * Install Blender’s scripts, datafiles and Python into the program filesystem from one index and
130
+ * concatenated blob. The Emscripten build uses its packed .data payload for the same content.
130
131
  */
131
132
  async function stageRuntime(
132
133
  filesystem: BrowserFileSystemLike,
@@ -149,20 +150,14 @@ async function stageRuntime(
149
150
  `Blender runtime ${entry.path}: bytes ${offset}..${offset + entry.size} lie outside ` +
150
151
  `the ${blob.byteLength}-byte runtime blob`,
151
152
  );
152
- // THE PACK'S RENDER OVERRIDE IS NOT STAGED. It is the delivery for a bare
153
- // `blender -b` with no session: at startup it asks a presenter at its own
154
- // directory and waits two seconds for one. This session registers the same
155
- // override itself (`session.py`, `VolterRenderEngine`) and answers through
156
- // its own channel, so here the module could only wait out those two
157
- // seconds (measured 2026-09-27: every boot printed "no presenter answered
158
- // at /tmp/vgai-presenter within 2 s").
153
+ // Skip the pack’s standalone render override: session.py registers VolterRenderEngine and serves its own presenter channel.
159
154
  if (PACK_RENDER_OVERRIDES.has(entry.path)) {
160
155
  offset += entry.size;
161
156
  continue;
162
157
  }
163
158
  const path = `${RESOURCES}/${entry.path}`;
164
159
  filesystem.mkdirSync(path.slice(0, path.lastIndexOf('/')), { recursive: true });
165
- filesystem.writeFileSync(path, blob.subarray(offset, offset + entry.size));
160
+ replaceFileSync(filesystem, path, blob.subarray(offset, offset + entry.size));
166
161
  offset += entry.size;
167
162
  }
168
163
  if (offset !== blob.byteLength)
@@ -181,15 +176,7 @@ type LayerFromIndex = (
181
176
  options: { mtimeMs: number; include?: (path: string) => boolean },
182
177
  ) => unknown;
183
178
 
184
- /**
185
- * Blender's runtime tree as a SHARED LAYER the program reads, never written
186
- * into this page's filesystem (browser-substrate ADR-0044 §6): the blob is
187
- * read once, straight into shared memory, and the index names where each file
188
- * lies in it. Staged file by file, the tree was held twice -- in this page's
189
- * filesystem and in the layer the substrate made of it for the program's
190
- * workers -- and took 0.3-0.5 s of every boot (measured 2026-09-27). A
191
- * substrate without the door (`sharedFileLayerFromIndex`) stages as before.
192
- */
179
+ /** Build a shared runtime file layer from one blob and its index. The program reads the layer without copying it into the page filesystem. */
193
180
  async function sharedRuntime(
194
181
  fromIndex: LayerFromIndex,
195
182
  log: BlenderEngineOptions['log'],
@@ -220,12 +207,7 @@ async function sharedRuntime(
220
207
  throw new Error(`Blender runtime index accounts for ${total} bytes; the blob served ${filled}`);
221
208
  const layer = fromIndex(RESOURCES, index.files, shared, {
222
209
  mtimeMs: Date.now(),
223
- // THE PACK'S RENDER OVERRIDE IS LEFT OUT: it is the delivery for a bare
224
- // `blender -b` with no session, and at startup it asks a presenter at its
225
- // own directory and waits two seconds for one. This session registers the
226
- // same override itself (`session.py`, `VolterRenderEngine`) and answers
227
- // through its own channel, so here the module could only wait (measured
228
- // 2026-09-27: every boot printed "no presenter answered ... within 2 s").
210
+ // Skip the standalone render override because session.py registers VolterRenderEngine on this session’s channel.
229
211
  include: (path) => !PACK_RENDER_OVERRIDES.has(path),
230
212
  });
231
213
  log('log', `blender: shared Blender's ${index.files.length}-file runtime tree in ${Math.round(performance.now() - began)} ms`);
@@ -239,16 +221,7 @@ type StoreLayerFromIndex = (
239
221
  options: { mtimeMs: number; include?: (path: string) => boolean },
240
222
  ) => unknown;
241
223
 
242
- /**
243
- * Blender's runtime tree as a STORE ON THE ORIGIN'S DISK the program reads
244
- * (browser-substrate ADR-0048): installed once per version by the
245
- * substrate's installer worker, and read file by file as Blender opens them,
246
- * a large file by the ranges it reads. The shared layer before it copied all
247
- * 96 MB into memory before Blender started (0.9 s of each boot) and held it
248
- * for the run; a boot reads 268 of the 2,082 files (measured 2026-09-28).
249
- * Undefined, and said, where it cannot be installed: the shared layer then
250
- * carries the tree as before.
251
- */
224
+ /** Install the versioned runtime file store on the origin and read files by range as Blender requests them. The shared layer carries the tree when installation is unavailable. */
252
225
  async function installedRuntime(
253
226
  fromIndex: StoreLayerFromIndex,
254
227
  hold: ((projectId: string) => Promise<void>) | undefined,
@@ -354,7 +327,7 @@ function programFiles(filesystem: BrowserFileSystemLike): BlenderFiles {
354
327
  }) } : {}),
355
328
  readFile: (path) => filesystem.readFile(path),
356
329
  // Not `async`: a write that throws throws here, so `request` never starts `.done` after a failed `.json`.
357
- writeFile: (path, data) => { filesystem.writeFileSync(path, data); return Promise.resolve(); },
330
+ writeFile: (path, data) => { replaceFileSync(filesystem, path, data); return Promise.resolve(); },
358
331
  mkdirTree: async (path) => filesystem.mkdirSync(path, { recursive: true }),
359
332
  readdir: async (path) => filesystem.readdirSync(path),
360
333
  stat: async (path) => {
@@ -377,10 +350,11 @@ export async function startWaliBlenderEngine(
377
350
  status: BlenderArtifactStatus,
378
351
  ): Promise<BlenderEngine> {
379
352
  const started = performance.now();
380
- const [wali, runtime, layers] = await Promise.all([
353
+ const [wali, runtime, layers, addonFiles] = await Promise.all([
381
354
  substrate('browser-wali/worker-program.js'),
382
355
  substrate('browser-runtime/browser-memory-filesystem.js'),
383
356
  substrate('browser-wali/shared-file-layer.js').catch((): Record<string, unknown> => ({})),
357
+ sessionAddonFiles(),
384
358
  ]);
385
359
  const WaliWorkerProgram = wali['BrowserWaliWorkerProgram'] as
386
360
  | (new (
@@ -398,12 +372,7 @@ export async function startWaliBlenderEngine(
398
372
  'does not know.',
399
373
  );
400
374
 
401
- // THE MODULE LOADS WHILE THE RUNTIME LAYER IS BUILT. Blender's 93 MB module
402
- // was fetched and compiled by the program's worker only once the program
403
- // ran, after the layer below: measured 2026-09-28, the layer 270-975 ms and
404
- // then the module 200-250 ms warm, in series. The substrate's preload fills
405
- // the cache the program's worker reads; a substrate without it, or a preload
406
- // that fails, leaves the worker to load the module as before.
375
+ // Preload the program module while constructing the runtime layer so fetch and compilation can overlap.
407
376
  const preload = wali['preloadWaliModule'] as ((url: string, integrity: string) => Promise<void>) | undefined;
408
377
  if (preload && status.integrity) void preload(artifactUrl('blender.wasm'), status.integrity).catch(() => undefined);
409
378
 
@@ -427,7 +396,12 @@ export async function startWaliBlenderEngine(
427
396
  const runtimeLayer = (fromStore ? await installedRuntime(fromStore, holdTree, options.log) : undefined)
428
397
  ?? (fromIndex ? await sharedRuntime(fromIndex, options.log) : undefined);
429
398
  if (!runtimeLayer) await stageRuntime(filesystem, options.log);
430
- filesystem.writeFileSync(SESSION_SCRIPT, sessionPython);
399
+ replaceFileSync(filesystem, SESSION_SCRIPT, sessionPython);
400
+ replaceFileSync(filesystem, `${SESSION_ROOT}/gltf_export.py`, gltfExportPython);
401
+ for (const { path, bytes } of addonFiles) {
402
+ filesystem.mkdirSync(path.slice(0, path.lastIndexOf('/')), { recursive: true });
403
+ replaceFileSync(filesystem, path, bytes);
404
+ }
431
405
 
432
406
  const workers = status.workers ?? { pool: 16, blender: 4 };
433
407
  const program = new WaliWorkerProgram(filesystem, {
@@ -457,22 +431,11 @@ export async function startWaliBlenderEngine(
457
431
  VOLTER_SESSION_ROOT: SESSION_ROOT,
458
432
  // THE ARENA'S DOOR ON THIS SKEW. See `readArena`.
459
433
  VOLTER_EXPORT_BUFFER_PATH: ARENA_PATH,
460
- // OPENIMAGEIO'S POOL AT ONE THREAD, as Blender's own are (`-t 1`,
461
- // BLENDER_WALI_WORKERS). Its default pool is built at the CPU count
462
- // before `-t` reaches it: measured 2026-09-28, nine of the ten threads
463
- // this Blender started were OpenImageIO's, idle for the session, each
464
- // holding a pool worker (browser-substrate `programs/blender/5.2.0`
465
- // README §(f)).
434
+ // Limit OpenImageIO’s pool before Blender starts; -t applies to Blender’s own worker pool.
466
435
  OPENIMAGEIO_THREADS: '1',
467
436
  },
468
437
  ...(runtimeLayer ? { layers: [runtimeLayer] } : {}),
469
- // BLENDER'S /tmp IS ITS OWN, as it is in the standalone skew's WasmFS:
470
- // what it writes there never crosses to this page, so nothing waits for
471
- // the page to take it (browser-substrate `privateRoots`, ADR-0046).
472
- // Measured 2026-09-27: published, a `.blend` saved to /tmp cost 11 ms
473
- // against the standalone skew's 2, most of it the page taking the file
474
- // before Blender's next printed line. The session's channel and the
475
- // project live elsewhere and still cross.
438
+ // Keep Blender’s /tmp private to the program. The session channel and project files remain shared with the page.
476
439
  privateRoots: ['/tmp'],
477
440
  threadPoolSize: workers.pool,
478
441
  // Blender talks to nothing. The editor's own routes are this worker's, not
@@ -486,10 +449,7 @@ export async function startWaliBlenderEngine(
486
449
  for (const line of text.split('\n')) {
487
450
  if (line === '') continue;
488
451
  if (line.startsWith('@@VOLTER-READY ')) readyLine = line.slice('@@VOLTER-READY '.length);
489
- // BLENDER'S OWN STREAMS ARE PAGE OUTPUT, NOT EDITOR-CONSOLE CONDITIONS
490
- // -- the same rule as the standalone engine, and for the same measured
491
- // reason: only the session's own named conditions belong in the set an
492
- // agent must drive to zero.
452
+ // Route Blender streams to page output; named session conditions use their own channel.
493
453
  options.log(sessionLevel(line), line);
494
454
  }
495
455
  };
@@ -518,14 +478,8 @@ export async function startWaliBlenderEngine(
518
478
  {
519
479
  onStdout: say,
520
480
  onStderr: say,
521
- // NOT Blender's stream, and that is exactly why this one IS a named
522
- // condition where `say` is page output. `onDiagnostic` is the WALI
523
- // runtime's own word to its host -- today, the one-shot notice that an
524
- // unimplemented syscall was answered `-ENOSYS` instead of trapping
525
- // (`@volter/browser-wali`'s `runtime.ts`). Answering like a kernel is
526
- // only defensible because the CALLER can degrade VISIBLY; without this
527
- // the degrade is silent, and before it existed the notice reached a
528
- // nested worker's devtools console and nothing else.
481
+ // Forward the WALI runtime’s unimplemented-syscall diagnostic to page output so a degraded
482
+ // operation remains visible to the caller.
529
483
  onDiagnostic: (message: string) => options.log('error', `@@VOLTER-ERROR ${message}`),
530
484
  },
531
485
  )
@@ -1,9 +1,6 @@
1
- /** Anything thrown, rendered so the message SURVIVES the worker boundary.
2
- *
3
- * String(object) erased a prior real diagnostic as [object Object]. Preserve
4
- * named messages or JSON fields, with a safe fallback for cyclic values.
5
- * Startup wrappers omit Error stacks from their user-visible message; worker
6
- * replies keep them as before. */
1
+ /** Preserve thrown messages across the worker boundary, including named fields on plain
2
+ * objects and a printable representation of cyclic values. Startup wrappers omit Error stacks;
3
+ * worker replies include them. */
7
4
  export function describeThrown(error: unknown, includeStack = true): string {
8
5
  if (error instanceof Error)
9
6
  return (includeStack ? error.stack : undefined) ?? `${error.name}: ${error.message}`;
@@ -1,19 +1,7 @@
1
- /**
2
- * A saved `.blend` cut into content-defined chunks, the unit a document save travels in
3
- * (`editor-blender/serving/blender-routes.ts`, `/__editor/blender-document`).
4
- *
5
- * WHY CONTENT-DEFINED. A save rewrites the whole file, but an edit changes little of it:
6
- * MEASURED 2026-09-29, native Blender 5.2 on the Stoneguard bridge, one object moved between two
7
- * saves changed 1 of 475 one-MiB chunks of a 497 MB file. Fixed offsets would keep that only while
8
- * no block changes size; an edit that grows a mesh would shift every chunk after it. Cutting where
9
- * the bytes themselves say (a gear hash, as FastCDC does) re-aligns right after the change, so only
10
- * the chunks around it are new.
11
- *
12
- * The bounds are the transport's: no chunk above {@link CHUNK_MAX}, which the server refuses.
13
- */
1
+ /** Split saved .blend bytes at content-defined gear-hash boundaries so unchanged ranges retain their identities after size-changing edits. CHUNK_MAX is the server’s transport limit. */
14
2
 
15
- export const CHUNK_MIN = 256 * 1024;
16
- export const CHUNK_MAX = 4 * 1024 * 1024;
3
+ const CHUNK_MIN = 256 * 1024;
4
+ const CHUNK_MAX = 4 * 1024 * 1024;
17
5
  /** 20 bits: a cut about every MiB past the minimum. */
18
6
  const CUT_MASK = (1 << 20) - 1;
19
7
 
@@ -32,7 +20,7 @@ const GEAR = (() => {
32
20
  })();
33
21
 
34
22
  /** The end offsets of each chunk of `bytes`. */
35
- export function chunkBoundaries(bytes: Uint8Array): number[] {
23
+ function chunkBoundaries(bytes: Uint8Array): number[] {
36
24
  const ends: number[] = [];
37
25
  let start = 0;
38
26
  while (start < bytes.length) {