@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.
- package/LICENSE +16 -0
- package/README.md +4 -11
- package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
- package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
- package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
- package/browser/addons/vrm/README.txt +3 -0
- package/browser/addons/vrm/__init__.py +41 -0
- package/browser/addons/vrm/blender_manifest.toml +16 -0
- package/browser/addons/vrm/common/__init__.py +1 -0
- package/browser/addons/vrm/common/animation.py +58 -0
- package/browser/addons/vrm/common/blender_manifest.py +82 -0
- package/browser/addons/vrm/common/char.py +16 -0
- package/browser/addons/vrm/common/convert.py +287 -0
- package/browser/addons/vrm/common/convert_any.py +48 -0
- package/browser/addons/vrm/common/debug.py +40 -0
- package/browser/addons/vrm/common/deep.py +162 -0
- package/browser/addons/vrm/common/error_dialog.py +274 -0
- package/browser/addons/vrm/common/fs.py +71 -0
- package/browser/addons/vrm/common/gl.py +20 -0
- package/browser/addons/vrm/common/gltf.py +1149 -0
- package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
- package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
- package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
- package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
- package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
- package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
- package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
- package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
- package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
- package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
- package/browser/addons/vrm/common/legacy_gltf.py +16 -0
- package/browser/addons/vrm/common/logger.py +84 -0
- package/browser/addons/vrm/common/mtoon0.blend +0 -0
- package/browser/addons/vrm/common/mtoon1.blend +0 -0
- package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
- package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
- package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
- package/browser/addons/vrm/common/ops/__init__.py +10 -0
- package/browser/addons/vrm/common/ops/export_scene.py +67 -0
- package/browser/addons/vrm/common/ops/icyp.py +53 -0
- package/browser/addons/vrm/common/ops/import_scene.py +57 -0
- package/browser/addons/vrm/common/ops/vrm.py +2380 -0
- package/browser/addons/vrm/common/ops/wm.py +167 -0
- package/browser/addons/vrm/common/preferences.py +362 -0
- package/browser/addons/vrm/common/progress.py +87 -0
- package/browser/addons/vrm/common/rotation.py +89 -0
- package/browser/addons/vrm/common/safe_removal.py +46 -0
- package/browser/addons/vrm/common/scene_watcher.py +230 -0
- package/browser/addons/vrm/common/shader.py +2243 -0
- package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
- package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
- package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
- package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
- package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
- package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
- package/browser/addons/vrm/common/version.py +161 -0
- package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
- package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
- package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
- package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
- package/browser/addons/vrm/common/workspace.py +227 -0
- package/browser/addons/vrm/common/writable_context.py +77 -0
- package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
- package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
- package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
- package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
- package/browser/addons/vrm/documentation/en-us/index.md +41 -0
- package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
- package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
- package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
- package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
- package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
- package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
- package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
- package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
- package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
- package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
- package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
- package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
- package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
- package/browser/addons/vrm/editor/__init__.py +1 -0
- package/browser/addons/vrm/editor/extension.py +425 -0
- package/browser/addons/vrm/editor/extension_accessor.py +75 -0
- package/browser/addons/vrm/editor/handler.py +84 -0
- package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
- package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
- package/browser/addons/vrm/editor/make_armature.py +709 -0
- package/browser/addons/vrm/editor/menu.py +147 -0
- package/browser/addons/vrm/editor/migration.py +297 -0
- package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
- package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
- package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
- package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
- package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
- package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
- package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
- package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
- package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
- package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
- package/browser/addons/vrm/editor/ops.py +471 -0
- package/browser/addons/vrm/editor/panel.py +315 -0
- package/browser/addons/vrm/editor/property_group.py +888 -0
- package/browser/addons/vrm/editor/search.py +682 -0
- package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
- package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
- package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
- package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
- package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
- package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
- package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
- package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
- package/browser/addons/vrm/editor/subscription.py +123 -0
- package/browser/addons/vrm/editor/t_pose.py +713 -0
- package/browser/addons/vrm/editor/validation.py +1271 -0
- package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
- package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
- package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
- package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
- package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
- package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
- package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
- package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
- package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
- package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
- package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
- package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
- package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
- package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
- package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
- package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
- package/browser/addons/vrm/exporter/__init__.py +1 -0
- package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
- package/browser/addons/vrm/exporter/export_scene.py +930 -0
- package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
- package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
- package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
- package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
- package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
- package/browser/addons/vrm/external/__init__.py +1 -0
- package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
- package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
- package/browser/addons/vrm/external/rigify_support.py +1 -0
- package/browser/addons/vrm/importer/__init__.py +1 -0
- package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
- package/browser/addons/vrm/importer/file_handler.py +34 -0
- package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
- package/browser/addons/vrm/importer/import_scene.py +646 -0
- package/browser/addons/vrm/importer/license_validation.py +198 -0
- package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
- package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
- package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
- package/browser/addons/vrm/importer/vrm_diff.py +316 -0
- package/browser/addons/vrm/locale/__init__.py +1 -0
- package/browser/addons/vrm/locale/ja_jp.py +826 -0
- package/browser/addons/vrm/locale/ruff.toml +8 -0
- package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
- package/browser/addons/vrm/locale/zh_hans.py +928 -0
- package/browser/addons/vrm/registration.py +670 -0
- package/browser/addons/vrm.json +18 -0
- package/browser/blender-emscripten-engine.mts +60 -128
- package/browser/blender-engine.mts +20 -186
- package/browser/blender-wali-engine.mts +35 -81
- package/browser/describe-thrown.mts +3 -6
- package/browser/document-chunks.mts +4 -16
- package/browser/gltf_export.py +3161 -0
- package/browser/heap-growth.mts +23 -0
- package/browser/index.ts +2 -0
- package/browser/protocol.ts +119 -35
- package/browser/pull-job.mts +2 -10
- package/browser/rna.ts +190 -73
- package/browser/runtime.ts +197 -82
- package/browser/session-addons.mts +33 -0
- package/browser/session-frame.mts +5 -24
- package/browser/session.py +1338 -1427
- package/browser/three/attach-presenter.ts +11 -39
- package/browser/three/blender-base64.ts +1 -36
- package/browser/three/blender-corner-normals.ts +1 -23
- package/browser/three/blender-display-shaders.json +1 -1
- package/browser/three/blender-display-shaders.lut +0 -0
- package/browser/three/blender-display-transform.ts +2 -1
- package/browser/three/blender-frame-columns.ts +5 -13
- package/browser/three/blender-graph-material.ts +18 -3
- package/browser/three/blender-node-graph.ts +10 -34
- package/browser/three/blender-physical-material.ts +2 -31
- package/browser/three/blender-runtime-armature.ts +7 -60
- package/browser/three/blender-runtime-frame.ts +9 -23
- package/browser/three/blender-runtime-geometry.ts +22 -26
- package/browser/three/blender-runtime-lighting.ts +48 -178
- package/browser/three/blender-runtime-shadows.ts +187 -1
- package/browser/three/blender-runtime-skeleton.ts +53 -1
- package/browser/three/blender-runtime-view.ts +505 -283
- package/browser/three/blender-runtime-weights.ts +9 -72
- package/browser/three/blender-sky.ts +13 -33
- package/browser/three/blender-triangulate.ts +7 -35
- package/browser/three/blender-viewport-sampling.ts +235 -0
- package/browser/three/blender-workbench-material.ts +25 -29
- package/browser/three/blender-workbench-outline.ts +199 -0
- package/browser/three/blender-world-volume.ts +62 -10
- package/browser/three/presenter.ts +10 -26
- package/browser/three/release.ts +5 -21
- package/browser/three/sky-precompute-worker.ts +1 -18
- package/browser/three/sky-worker.ts +5 -23
- package/browser/three/world-field-sampler.ts +12 -26
- package/browser/volter_three.py +9 -105
- package/browser/worker.ts +156 -151
- package/package.json +5 -2
- package/wasm/BUNDLE.json +17 -17
- package/wasm/DEPENDENCY-LICENSES.txt +14 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
- package/browser/frame-transfer.mts +0 -19
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/blender-agx.ts +0 -235
- package/browser/three/blender-display-lut.ts +0 -145
- package/browser/three/blender-filmic.ts +0 -49
- package/browser/three/blender-standard.ts +0 -68
- package/browser/three/blender-world-visibility.ts +0 -278
- package/browser/three/filmic-srgb.lut +0 -0
package/browser/runtime.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { PullResult } from './pull-job.mts';
|
|
2
|
+
import { describeThrown } from './describe-thrown.mts';
|
|
2
3
|
import { FrameStreamReader } from './frame-stream.mts';
|
|
4
|
+
import { HEAP_GROWTH_REFUSED } from './protocol';
|
|
3
5
|
/**
|
|
4
|
-
* The tab-side handle on a Blender session: spawns the worker, forwards
|
|
5
|
-
*
|
|
6
|
+
* The tab-side handle on a Blender session: spawns the worker, forwards wire
|
|
7
|
+
* requests, and hands every presented frame to whoever displays it.
|
|
6
8
|
* One instance per editor tab; the session and its model die with the worker.
|
|
7
9
|
*/
|
|
8
10
|
import type {
|
|
9
11
|
CaptureRequest,
|
|
10
|
-
FileEntry,
|
|
11
12
|
RuntimeStart,
|
|
12
13
|
WorkerReply,
|
|
13
14
|
WorkerRequest,
|
|
@@ -15,9 +16,16 @@ import type {
|
|
|
15
16
|
NativePreviewRequest,
|
|
16
17
|
NativePreviewResult,
|
|
17
18
|
FileListSnapshot,
|
|
19
|
+
GltfExportAnswer,
|
|
20
|
+
GltfExportKind,
|
|
21
|
+
GltfExportPrevious,
|
|
22
|
+
ScriptRunAnswer,
|
|
23
|
+
BlenderAssetLibrary,
|
|
18
24
|
} from './protocol';
|
|
19
25
|
import type {
|
|
20
26
|
BlenderActionClip,
|
|
27
|
+
BlenderSceneMovie,
|
|
28
|
+
BlenderSoundScene,
|
|
21
29
|
BlenderNodeTree,
|
|
22
30
|
BlenderOutlinerTree,
|
|
23
31
|
BlenderOutlinerWrite,
|
|
@@ -30,6 +38,17 @@ import type {
|
|
|
30
38
|
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
31
39
|
type Request = DistributiveOmit<WorkerRequest, 'id'>;
|
|
32
40
|
|
|
41
|
+
|
|
42
|
+
/** A script every save of a document runs in the save's own turn (`BlenderRuntime.saveDocument`), as the caller names
|
|
43
|
+
* it for the document saved: null for none. */
|
|
44
|
+
export type SaveScript = (document: string) => Promise<{ readonly source: string; readonly args: readonly string[] }> | null;
|
|
45
|
+
|
|
46
|
+
/** A save that landed: the document it wrote, and what its script did on the scene it wrote (null where none ran). */
|
|
47
|
+
export interface SavedDocument {
|
|
48
|
+
readonly document: string;
|
|
49
|
+
readonly script: ScriptRunAnswer | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
export interface BlenderRuntimeOptions {
|
|
34
53
|
/** Stage one verified mesh/image without replacing the displayed frame. */
|
|
35
54
|
stage?(part: { session: string; revision: number; evaluation?: 'render'; abort?: boolean; mesh?: string; image?: string; piece?: unknown }): Promise<void> | void;
|
|
@@ -56,17 +75,14 @@ export interface BlenderRuntimeOptions {
|
|
|
56
75
|
* names the new file and before the call that moved it answers. An observer: a throw here
|
|
57
76
|
* changes nothing. */
|
|
58
77
|
documentMoved?(moved: { readonly from: string; readonly document: string }): void;
|
|
78
|
+
/** The worker's save refusal or its recovery, for the named project document. */
|
|
79
|
+
documentSaving?(state: { readonly document: string; readonly note: string | null }): void;
|
|
80
|
+
/** The document changed (`true`, at every change) or became its file again (`false`). An
|
|
81
|
+
* observer: a throw here changes nothing. */
|
|
82
|
+
documentDirty?(dirty: boolean): void;
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
/**
|
|
62
|
-
* What the tab answers a present with.
|
|
63
|
-
*
|
|
64
|
-
* `held` is WHAT THE PRESENTER HELD BEFORE THIS FRAME — the presenter's own
|
|
65
|
-
* report of its state, which the session compares against its record of what
|
|
66
|
-
* it sent (`session.py::_present`). `null` says it held nothing; an ABSENT
|
|
67
|
-
* field says this presenter does not report one (an older editor integration), and
|
|
68
|
-
* the session then judges nothing rather than resetting on every frame.
|
|
69
|
-
*/
|
|
85
|
+
/** The presenter’s state before this frame. held is null when it holds no frame; the session compares a reported state with what it sent. */
|
|
70
86
|
export interface PresentAnswer {
|
|
71
87
|
capture?: unknown;
|
|
72
88
|
held?: { session: string; revision: number } | null;
|
|
@@ -94,24 +110,7 @@ export interface ScreenshotView {
|
|
|
94
110
|
target?: number[];
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
/**
|
|
98
|
-
* HOW LONG THE WORKER TOOK, measured by the PAGE.
|
|
99
|
-
*
|
|
100
|
-
* WHY THE PAGE MEASURES (2026-09-16): a single `blender-execute` held the
|
|
101
|
-
* worker for over 1,800s and wedged the tab, and the only report anyone got
|
|
102
|
-
* was a replay harness timing out. A blocked worker cannot report on itself —
|
|
103
|
-
* the loop that would send the number is the loop that is stuck — so the one
|
|
104
|
-
* vantage point that still works is the side that POSTED the message. Every
|
|
105
|
-
* call goes through {@link BlenderRuntime.metrics}'s meter, which stamps the
|
|
106
|
-
* `postMessage` and the reply.
|
|
107
|
-
*
|
|
108
|
-
* MEASUREMENT ONLY. Nothing here cancels, kills or budgets a call: a budget is
|
|
109
|
-
* a policy decision, and this is the number a policy would have to be made
|
|
110
|
-
* from.
|
|
111
|
-
*
|
|
112
|
-
* Times are milliseconds on `performance.now()`. Counters are monotonic since
|
|
113
|
-
* this runtime was constructed (one per page load).
|
|
114
|
-
*/
|
|
113
|
+
/** Measure worker calls from the page between postMessage and reply, so a blocked worker remains observable. Metrics use performance.now milliseconds and monotonic counters for this runtime; they impose no call budget. */
|
|
115
114
|
export interface BlenderCallMetrics {
|
|
116
115
|
/** Age of the OLDEST outstanding call, or null when the worker is idle.
|
|
117
116
|
* This is the field that has a number during the wedge — the counters
|
|
@@ -124,10 +123,10 @@ export interface BlenderCallMetrics {
|
|
|
124
123
|
* exactly as long as it keeps running. */
|
|
125
124
|
readonly maxCallMs: number | null;
|
|
126
125
|
/** Operation and load boundary owning the longest request; no payload data. */
|
|
127
|
-
readonly maxCallLabel
|
|
126
|
+
readonly maxCallLabel: string | null;
|
|
128
127
|
/** Current cooperative phase and count of settled wire requests; sampled, not pushed per unit. */
|
|
129
|
-
readonly currentPhase
|
|
130
|
-
readonly completedCalls
|
|
128
|
+
readonly currentPhase: string | null;
|
|
129
|
+
readonly completedCalls: number;
|
|
131
130
|
/** Calls whose duration passed 5s, counting outstanding ones already past it. */
|
|
132
131
|
readonly callsOver5s: number;
|
|
133
132
|
/** The same at 30s — a call this side of it is slow, past it is a wedge. */
|
|
@@ -153,12 +152,22 @@ export interface BlenderCallMetrics {
|
|
|
153
152
|
* `blender_web_headless.cmake`). JavaScript cannot read a memory's maximum back. */
|
|
154
153
|
const ENGINE_MEMORY_CEILING_MB = 4096;
|
|
155
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Requests in OVERTAKES may overtake queued OVERTAKEN document reads. Export and read-file
|
|
157
|
+
* operations keep their order relative to edits; skin, movie, Properties, Outliner, node and UV
|
|
158
|
+
* inspections can yield their queue position.
|
|
159
|
+
*/
|
|
160
|
+
const OVERTAKES: ReadonlySet<string> = new Set(['gltf-export', 'read-file']);
|
|
161
|
+
const OVERTAKEN: ReadonlySet<string> = new Set(['action-clip', 'scene-movie', 'rna', 'rna-context', 'outliner', 'node-tree', 'uv-layout']);
|
|
162
|
+
|
|
156
163
|
export class BlenderRuntime {
|
|
157
164
|
readonly #worker: Worker;
|
|
158
165
|
readonly #options: BlenderRuntimeOptions;
|
|
159
166
|
/** Blender's own last output lines, which are what says why it died when it dies: they reach
|
|
160
167
|
* the page as ordinary output, so the failure below carries them. */
|
|
161
168
|
readonly #lastLines: string[] = [];
|
|
169
|
+
/** The line a thread printed when the browser refused to grow the heap, kept for the failure it causes. */
|
|
170
|
+
#growthRefused: string | null = null;
|
|
162
171
|
readonly #pending = new Map<
|
|
163
172
|
number,
|
|
164
173
|
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
@@ -166,6 +175,8 @@ export class BlenderRuntime {
|
|
|
166
175
|
#nextId = 0;
|
|
167
176
|
#started: Promise<RuntimeStart> | null = null;
|
|
168
177
|
#stopping: Promise<void> | null = null;
|
|
178
|
+
/** The script runs this runtime has made, each in a directory of its own. */
|
|
179
|
+
#scriptRuns = 0;
|
|
169
180
|
#terminated = false;
|
|
170
181
|
#dirty = false;
|
|
171
182
|
readonly #beforeUnload = (event: BeforeUnloadEvent): void => {
|
|
@@ -198,33 +209,28 @@ export class BlenderRuntime {
|
|
|
198
209
|
globalThis.addEventListener?.('beforeunload', this.#beforeUnload);
|
|
199
210
|
this.#worker.onmessage = (event: MessageEvent<WorkerReply>) => void this.#receive(event.data);
|
|
200
211
|
this.#worker.onerror = (event) => {
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// nothing. Every field the event carries goes in: the thrown error's own
|
|
204
|
-
// message and stack when there is one, and the file and line otherwise,
|
|
205
|
-
// which is what a failed nested import leaves behind.
|
|
212
|
+
// Worker errors include the thrown stack or message and the source location.
|
|
213
|
+
// Failed module imports may provide only the file and line.
|
|
206
214
|
const thrown = event.error as Error | undefined;
|
|
207
215
|
const where = event.filename ? ` at ${event.filename}:${event.lineno}:${event.colno}` : '';
|
|
208
216
|
const said = thrown?.stack ?? thrown?.message ?? event.message;
|
|
209
|
-
//
|
|
210
|
-
// Error ("Uncaught [object Object]"), while the reason (an allocation that failed, the file
|
|
211
|
-
// it was reading) is the last thing Blender printed.
|
|
217
|
+
// Include Blender's retained output alongside the worker error.
|
|
212
218
|
const last = this.#lastLines.length > 0 ? `\nBlender's last output:\n${this.#lastLines.join('\n')}` : '';
|
|
213
219
|
// OUT OF MEMORY IS SAID AS SUCH, from the heap the worker reported as the thread died.
|
|
214
220
|
const heapMB = this.#wasmBytes === null ? null : Math.round(this.#wasmBytes / 1048576);
|
|
215
|
-
const
|
|
216
|
-
|
|
221
|
+
const refused = this.#growthRefused;
|
|
222
|
+
const exhausted = refused !== null
|
|
223
|
+
? `Blender ran out of memory: the browser would not give its heap more (${refused.slice(HEAP_GROWTH_REFUSED.length + 2)}), ` +
|
|
224
|
+
"below the in-tab engine's ceiling, which happens when the machine itself is short of memory. " +
|
|
225
|
+
'Free memory (other editors, tabs, builds) and run the call again. '
|
|
226
|
+
: heapMB !== null && heapMB >= ENGINE_MEMORY_CEILING_MB - 16
|
|
217
227
|
? `Blender ran out of memory: its heap reached ${heapMB} MB of the in-tab engine's ` +
|
|
218
228
|
`${ENGINE_MEMORY_CEILING_MB} MB, and this scene needs more. `
|
|
219
229
|
: '';
|
|
220
230
|
const error = new Error(
|
|
221
231
|
`${exhausted}Blender worker failed: ${said || 'the worker script did not load'}${where}${last}`,
|
|
222
232
|
);
|
|
223
|
-
//
|
|
224
|
-
// captures it, source-blind), and this package may import nothing of the
|
|
225
|
-
// editor to say it any other way. Without this line the failure reached
|
|
226
|
-
// a toast and `cyclotron console` read 0/0 while no Model document could open
|
|
227
|
-
// (measured 2026-09-20 from a registry install).
|
|
233
|
+
// Report initialization failures to the page console, which the editor captures into its session ledger.
|
|
228
234
|
console.error(error.message);
|
|
229
235
|
for (const id of [...this.#pending.keys()]) this.#settled(id);
|
|
230
236
|
for (const pending of this.#pending.values()) pending.reject(error);
|
|
@@ -436,6 +442,39 @@ export class BlenderRuntime {
|
|
|
436
442
|
}
|
|
437
443
|
|
|
438
444
|
|
|
445
|
+
/** Sampled scene animation for Timeline preview and export (`BlenderSceneMovie`). */
|
|
446
|
+
async sceneMovie(): Promise<BlenderSceneMovie> {
|
|
447
|
+
await this.#ready();
|
|
448
|
+
return (await this.#request({ op: 'scene-movie' })) as BlenderSceneMovie;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** THE SCENE'S SOUND for frames `first` to `last` (`./rna.ts`, `BlenderSoundScene`), for the page to mix. */
|
|
452
|
+
async soundScene(first: number, last: number): Promise<BlenderSoundScene> {
|
|
453
|
+
await this.#ready();
|
|
454
|
+
return (await this.#request({ op: 'sound-scene', first, last })) as BlenderSoundScene;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Export the document as a component or level through Blender’s glTF exporter into the module
|
|
460
|
+
* filesystem. The answer names the files for the caller to carry into the project; the call holds
|
|
461
|
+
* the session until completion.
|
|
462
|
+
*/
|
|
463
|
+
async gltfExport(options: { readonly kind: GltfExportKind; readonly force?: boolean; readonly previous?: GltfExportPrevious; readonly blendSha256?: string; readonly luts?: Readonly<Record<string, string>> }): Promise<GltfExportAnswer> {
|
|
464
|
+
await this.#ready();
|
|
465
|
+
return (await this.#request({ op: 'gltf-export', kind: options.kind,
|
|
466
|
+
...(options.force ? { force: true } : {}), ...(options.previous ? { previous: options.previous } : {}),
|
|
467
|
+
...(options.blendSha256 ? { blendSha256: options.blendSha256 } : {}), ...(options.luts ? { luts: options.luts } : {}) })) as GltfExportAnswer;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
/** An asset library in Blender's preferences, set again under its name (Preferences > File Paths). */
|
|
473
|
+
async setAssetLibrary(library: BlenderAssetLibrary): Promise<void> {
|
|
474
|
+
await this.#ready();
|
|
475
|
+
await this.#request({ op: 'asset-library', library });
|
|
476
|
+
}
|
|
477
|
+
|
|
439
478
|
/** Write ONE restriction column (the eye, the render camera, a collection's
|
|
440
479
|
* Exclude). A column Blender draws on no row of that type is refused by
|
|
441
480
|
* name rather than written somewhere near it. */
|
|
@@ -449,16 +488,7 @@ export class BlenderRuntime {
|
|
|
449
488
|
})) as BlenderOutlinerWrite;
|
|
450
489
|
}
|
|
451
490
|
|
|
452
|
-
/**
|
|
453
|
-
* PRESENT WHAT THE ENGINE HOLDS, with no capture.
|
|
454
|
-
*
|
|
455
|
-
* Every mutation presents (`session.py::dispatch`), so in the ordinary
|
|
456
|
-
* course of a session nothing needs to ask. The exception is the first
|
|
457
|
-
* frame: opening a `.blend` is not a mutation, so a freshly opened Model
|
|
458
|
-
* document had nothing to display until something RAN — measured by I1 and
|
|
459
|
-
* fixed here (WORK.md §Blender in the tab is Blender, "Inspection parity",
|
|
460
|
-
* I2 decision 6). The document asks on mount.
|
|
461
|
-
*/
|
|
491
|
+
/** Present the engine’s current state without capture. Documents ask on mount so an opened .blend draws before its first mutation. */
|
|
462
492
|
async present(): Promise<void> {
|
|
463
493
|
await this.#ready();
|
|
464
494
|
await this.#request({ op: 'present' });
|
|
@@ -472,6 +502,38 @@ export class BlenderRuntime {
|
|
|
472
502
|
| { error: string };
|
|
473
503
|
}
|
|
474
504
|
|
|
505
|
+
/** Whether the document holds changes its project file does not. */
|
|
506
|
+
get dirty(): boolean {
|
|
507
|
+
return this.#dirty;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** A save of the document, and in the same turn, before any other request reaches this Blender, the script `after`
|
|
511
|
+
* names for the document it wrote (as the worker holds it once saved, a move accepted before it included), run on
|
|
512
|
+
* the scene just saved, in a working directory of its own, its files carried back in its answer. Null when there
|
|
513
|
+
* was nothing to save. */
|
|
514
|
+
async saveDocument(after?: SaveScript): Promise<SavedDocument | null> {
|
|
515
|
+
await this.#ready();
|
|
516
|
+
return this.#inTurn('flush-document', false, () => this.#save(after));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** The document's bytes as a save would write them, carried nowhere. */
|
|
520
|
+
async snapshotDocument(): Promise<Uint8Array> {
|
|
521
|
+
await this.#ready();
|
|
522
|
+
return (await this.#request({ op: 'snapshot-document' })) as Uint8Array;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Reopen the document from its project file, discarding its changes. */
|
|
526
|
+
async revertDocument(): Promise<void> {
|
|
527
|
+
await this.#ready();
|
|
528
|
+
await this.#request({ op: 'revert-document' });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Reopen the document as `bytes`, its unsaved state. */
|
|
532
|
+
async restoreDocument(bytes: Uint8Array): Promise<void> {
|
|
533
|
+
await this.#ready();
|
|
534
|
+
await this.#request({ op: 'restore-document', bytes });
|
|
535
|
+
}
|
|
536
|
+
|
|
475
537
|
async readFile(path: string): Promise<Uint8Array> {
|
|
476
538
|
await this.#ready();
|
|
477
539
|
return (await this.#request({ op: 'read-file', path })) as Uint8Array;
|
|
@@ -482,11 +544,6 @@ export class BlenderRuntime {
|
|
|
482
544
|
await this.#request({ op: 'write-file', path, bytes });
|
|
483
545
|
}
|
|
484
546
|
|
|
485
|
-
async listFiles(path: string): Promise<FileEntry[]> {
|
|
486
|
-
await this.#ready();
|
|
487
|
-
return (await this.#request({ op: 'list-files', path })) as FileEntry[];
|
|
488
|
-
}
|
|
489
|
-
|
|
490
547
|
/** Files and their native document identity, without executing Python or
|
|
491
548
|
* exporting/saving the model. The worker serializes this with authoring. */
|
|
492
549
|
async listFilesSnapshot(path: string): Promise<FileListSnapshot> {
|
|
@@ -501,15 +558,18 @@ export class BlenderRuntime {
|
|
|
501
558
|
}
|
|
502
559
|
|
|
503
560
|
/** Drain accepted calls and persist the document before destroying its only
|
|
504
|
-
* copy. A failed save keeps the worker alive so the caller can retry.
|
|
505
|
-
|
|
561
|
+
* copy. A failed save keeps the worker alive so the caller can retry. With `retire`, the stop's save is the caller's
|
|
562
|
+
* too: it runs `retire.after`'s script in its turn, as every save does (`saveDocument`), and `landed` hears it. */
|
|
563
|
+
stop(retire?: { readonly after?: SaveScript; readonly landed: (saved: SavedDocument) => void }): Promise<void> {
|
|
506
564
|
if (this.#terminated) return Promise.resolve();
|
|
507
565
|
if (this.#stopping) return this.#stopping;
|
|
508
566
|
this.#stopping = (async () => {
|
|
509
567
|
// A start that failed opened no document, so there is nothing to flush: its stale
|
|
510
568
|
// rejection must not refuse every later close.
|
|
511
569
|
const started = this.#started ? await this.#started.then(() => true, () => false) : false;
|
|
512
|
-
|
|
570
|
+
// (one save, queued behind every request accepted before the stop; none is taken after it)
|
|
571
|
+
const saved = started ? await this.#inTurn('flush-document', true, () => this.#save(retire?.after)) : null;
|
|
572
|
+
if (saved !== null) retire?.landed(saved);
|
|
513
573
|
this.terminate();
|
|
514
574
|
})().catch(error => {
|
|
515
575
|
this.#stopping = null;
|
|
@@ -518,6 +578,23 @@ export class BlenderRuntime {
|
|
|
518
578
|
return this.#stopping;
|
|
519
579
|
}
|
|
520
580
|
|
|
581
|
+
/** The save, and its script on the scene it wrote, inside one turn. */
|
|
582
|
+
async #save(after: SaveScript | undefined): Promise<SavedDocument | null> {
|
|
583
|
+
const { saved } = (await this.#wire({ op: 'flush-document' })) as { saved: boolean };
|
|
584
|
+
const document = this.#document;
|
|
585
|
+
if (!saved || document === null) return null;
|
|
586
|
+
const named = after?.(document) ?? null;
|
|
587
|
+
if (named === null) return { document, script: null };
|
|
588
|
+
let script: { readonly source: string; readonly args: readonly string[] };
|
|
589
|
+
try {
|
|
590
|
+
script = await named;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
return { document, script: { output: '', error: `the save's script could not be read: ${describeThrown(error)}`, files: [], unstaged: [] } };
|
|
593
|
+
}
|
|
594
|
+
const dir = `/tmp/volter-script-run/${++this.#scriptRuns}`;
|
|
595
|
+
return { document, script: (await this.#wire({ op: 'run-script', source: script.source, args: script.args, dir })) as ScriptRunAnswer };
|
|
596
|
+
}
|
|
597
|
+
|
|
521
598
|
/** Forced teardown for a lost session; explicit user stops must use stop(). */
|
|
522
599
|
terminate(): void {
|
|
523
600
|
this.#discardStagedFrame();
|
|
@@ -546,7 +623,8 @@ export class BlenderRuntime {
|
|
|
546
623
|
* displaying the frame is main-thread time, which is the long-task observer's
|
|
547
624
|
* subject (the host's `worker-call-metrics.ts`).
|
|
548
625
|
*/
|
|
549
|
-
metrics(
|
|
626
|
+
metrics(): BlenderCallMetrics {
|
|
627
|
+
const now = performance.now();
|
|
550
628
|
let oldest: number | null = null;
|
|
551
629
|
let max = this.#maxCompletedMs;
|
|
552
630
|
let maxLabel = this.#maxCompletedLabel;
|
|
@@ -594,33 +672,61 @@ export class BlenderRuntime {
|
|
|
594
672
|
|
|
595
673
|
#completedCalls = 0;
|
|
596
674
|
#acceptedRequests = 0;
|
|
597
|
-
|
|
598
|
-
|
|
675
|
+
/**
|
|
676
|
+
* Process requests in queue order. gltf-export and read-file may overtake queued document reads
|
|
677
|
+
* in OVERTAKEN, while preserving the order of edits and other operations.
|
|
678
|
+
*/
|
|
679
|
+
#waiting: { readonly op: string; readonly start: () => void }[] = [];
|
|
680
|
+
#busy = false;
|
|
681
|
+
#enqueue(op: string, start: () => void): void {
|
|
682
|
+
const item = { op, start };
|
|
683
|
+
if (OVERTAKES.has(op)) {
|
|
684
|
+
let at = this.#waiting.length;
|
|
685
|
+
while (at > 0 && OVERTAKEN.has(this.#waiting[at - 1]!.op)) at--;
|
|
686
|
+
this.#waiting.splice(at, 0, item);
|
|
687
|
+
} else this.#waiting.push(item);
|
|
688
|
+
if (!this.#busy) this.#next();
|
|
689
|
+
}
|
|
690
|
+
#next(): void {
|
|
691
|
+
const item = this.#waiting.shift();
|
|
692
|
+
this.#busy = item !== undefined;
|
|
693
|
+
item?.start();
|
|
694
|
+
}
|
|
695
|
+
#request(request: Request): Promise<unknown> {
|
|
696
|
+
return this.#inTurn(request.op, false, () => this.#wire(request));
|
|
697
|
+
}
|
|
698
|
+
/** One turn of the queue: `run` alone talks to the worker until it is done, whatever it sends. */
|
|
699
|
+
#inTurn<T>(op: string, shutdown: boolean, run: () => Promise<T>): Promise<T> {
|
|
599
700
|
if (this.#terminated || (this.#stopping && !shutdown))
|
|
600
701
|
return Promise.reject(new Error('The Blender session is stopping or terminated'));
|
|
601
702
|
const began = performance.now();
|
|
602
703
|
let endWork = () => {};
|
|
603
704
|
this.#acceptedRequests++;
|
|
604
|
-
const
|
|
605
|
-
|
|
705
|
+
const turn = new Promise<void>((start) => this.#enqueue(op, start));
|
|
706
|
+
const answer = turn.then(async () => {
|
|
707
|
+
const finishWork = this.#work(`running worker ${op}`);
|
|
606
708
|
endWork = () => { if (this.#operationWork.delete(endWork)) finishWork(); };
|
|
607
709
|
this.#operationWork.add(endWork);
|
|
608
|
-
|
|
609
|
-
let result = await this.#wireRequest(request) as PullResult<unknown>;
|
|
610
|
-
while (result?.load === 'continue') {
|
|
611
|
-
this.#loadBoundary = result.phase;
|
|
612
|
-
result = await this.#wireRequest({ op: 'load-next', token: result.token }) as PullResult<unknown>;
|
|
613
|
-
}
|
|
614
|
-
return result?.load === 'done' ? result.value : result;
|
|
710
|
+
return run();
|
|
615
711
|
}).finally(() => {
|
|
616
712
|
this.#acceptedRequests--;
|
|
617
713
|
endWork();
|
|
618
714
|
const totalMs = Math.round(performance.now() - began);
|
|
619
|
-
try { if (totalMs >= 1000) this.#options.log?.('log', `@@VOLTER-WORK op=${
|
|
715
|
+
try { if (totalMs >= 1000) this.#options.log?.('log', `@@VOLTER-WORK op=${op} totalMs=${totalMs}`); } catch { /* diagnostics cannot poison the queue */ }
|
|
716
|
+
this.#next();
|
|
620
717
|
});
|
|
621
|
-
this.#requestTail = answer.then(() => {}, () => {});
|
|
622
718
|
return answer;
|
|
623
719
|
}
|
|
720
|
+
/** One request to the worker, a load's continuations included. */
|
|
721
|
+
async #wire(request: Request): Promise<unknown> {
|
|
722
|
+
this.#loadBoundary = request.op;
|
|
723
|
+
let result = await this.#wireRequest(request) as PullResult<unknown>;
|
|
724
|
+
while (result?.load === 'continue') {
|
|
725
|
+
this.#loadBoundary = result.phase;
|
|
726
|
+
result = await this.#wireRequest({ op: 'load-next', token: result.token }) as PullResult<unknown>;
|
|
727
|
+
}
|
|
728
|
+
return result?.load === 'done' ? result.value : result;
|
|
729
|
+
}
|
|
624
730
|
|
|
625
731
|
#wireRequest(request: Request): Promise<unknown> {
|
|
626
732
|
if (this.#terminated) return Promise.reject(new Error('The Blender session is terminated'));
|
|
@@ -683,6 +789,13 @@ export class BlenderRuntime {
|
|
|
683
789
|
}
|
|
684
790
|
if (reply.op === 'document-dirty') {
|
|
685
791
|
this.#dirty = reply.dirty;
|
|
792
|
+
try { this.#options.documentDirty?.(reply.dirty); }
|
|
793
|
+
catch { /* an observer cannot change whether the document changed */ }
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (reply.op === 'document-saving') {
|
|
797
|
+
try { this.#options.documentSaving?.({ document: reply.document, note: reply.note }); }
|
|
798
|
+
catch { /* an observer cannot change whether the document was saved */ }
|
|
686
799
|
return;
|
|
687
800
|
}
|
|
688
801
|
if (reply.op === 'document-moved') {
|
|
@@ -698,6 +811,8 @@ export class BlenderRuntime {
|
|
|
698
811
|
return;
|
|
699
812
|
}
|
|
700
813
|
if (reply.op === 'log') {
|
|
814
|
+
// (a heap the browser refused to grow is kept past the aborts it causes, which push it out of the last lines)
|
|
815
|
+
if (reply.text.startsWith(HEAP_GROWTH_REFUSED)) this.#growthRefused ??= reply.text;
|
|
701
816
|
this.#lastLines.push(reply.text);
|
|
702
817
|
if (this.#lastLines.length > 12) this.#lastLines.shift();
|
|
703
818
|
this.#options.log?.(reply.level, reply.text);
|
|
@@ -721,8 +836,8 @@ export class BlenderRuntime {
|
|
|
721
836
|
this.#worker.postMessage({
|
|
722
837
|
op: 'present-result',
|
|
723
838
|
id: reply.id,
|
|
724
|
-
...(answer
|
|
725
|
-
...(
|
|
839
|
+
...(answer.capture === undefined ? {} : { capture: answer.capture }),
|
|
840
|
+
...('held' in answer
|
|
726
841
|
? { held: answer.held }
|
|
727
842
|
: {}),
|
|
728
843
|
} satisfies WorkerRequest);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ADD-ONS THIS BLENDER SHIPS beside Blender's own, as the files each engine writes into the session's root before
|
|
3
|
+
* Blender starts (`<SESSION_ROOT>/addons/<package>/…`), where `session.py`'s `_enable_addons` enables every package it
|
|
4
|
+
* finds there for the whole session.
|
|
5
|
+
*
|
|
6
|
+
* - `addons/vrm`: saturday06's VRM Add-on for Blender ("VRM format"), the release `addons/vrm.json` names, as it was
|
|
7
|
+
* published. Blender reads its Python and its manifest (`common/version.py` reads `blender_manifest.toml` at import)
|
|
8
|
+
* and its MToon node-group libraries (`common/*.blend`); its documentation and licence texts stay in the package.
|
|
9
|
+
*
|
|
10
|
+
* Text crosses as the module's own source; a `.blend` is bytes, fetched from the URL the bundle gives it.
|
|
11
|
+
*/
|
|
12
|
+
import { SESSION_ROOT } from './blender-engine.mts';
|
|
13
|
+
|
|
14
|
+
const texts = import.meta.glob<string>(['./addons/vrm/**/*.py', './addons/vrm/blender_manifest.toml'], {
|
|
15
|
+
query: '?raw',
|
|
16
|
+
import: 'default',
|
|
17
|
+
eager: true,
|
|
18
|
+
});
|
|
19
|
+
const libraries = import.meta.glob<string>('./addons/vrm/**/*.blend', { query: '?url', import: 'default', eager: true });
|
|
20
|
+
|
|
21
|
+
/** Every add-on file, at its path in the module filesystem. */
|
|
22
|
+
export async function sessionAddonFiles(): Promise<readonly { readonly path: string; readonly bytes: Uint8Array }[]> {
|
|
23
|
+
const at = (key: string): string => `${SESSION_ROOT}/${key.slice('./'.length)}`;
|
|
24
|
+
const utf8 = new TextEncoder();
|
|
25
|
+
return [
|
|
26
|
+
...Object.entries(texts).map(([key, text]) => ({ path: at(key), bytes: utf8.encode(text) })),
|
|
27
|
+
...(await Promise.all(Object.entries(libraries).map(async ([key, url]) => {
|
|
28
|
+
const answer = await fetch(url);
|
|
29
|
+
if (!answer.ok) throw new Error(`The add-on file ${key.slice('./'.length)} could not be read: ${url} answered HTTP ${answer.status}`);
|
|
30
|
+
return { path: at(key), bytes: new Uint8Array(await answer.arrayBuffer()) };
|
|
31
|
+
}))),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
@@ -54,8 +54,9 @@ type Typed =
|
|
|
54
54
|
| Int16Array
|
|
55
55
|
| Uint16Array;
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
57
|
+
/**
|
|
58
|
+
* Column dtype vocabulary matching the C types written by the export door.
|
|
59
|
+
*/
|
|
59
60
|
const READERS: Record<string, (bytes: Uint8Array) => Typed> = {
|
|
60
61
|
f32: (b) => new Float32Array(b.buffer, b.byteOffset, b.byteLength >> 2),
|
|
61
62
|
f64: (b) => new Float64Array(b.buffer, b.byteOffset, b.byteLength >> 3),
|
|
@@ -113,34 +114,14 @@ export function columnsToTypedArrays(arena: Uint8Array, frame: unknown): unknown
|
|
|
113
114
|
|
|
114
115
|
/** A column as the RECORD of it: what it was, how many bytes, and their digest.
|
|
115
116
|
* Never the bytes — see `describeFrame`. */
|
|
116
|
-
|
|
117
|
+
interface ColumnDigest {
|
|
117
118
|
dtype: string;
|
|
118
119
|
length: number;
|
|
119
120
|
/** Lowercase hex SHA-256 of exactly the `length` bytes the descriptor names. */
|
|
120
121
|
sha256: string;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
/**
|
|
124
|
-
* THE FRAME THIS PRESENT SUBMITTED, as a record that can sit in a JSON file.
|
|
125
|
-
*
|
|
126
|
-
* The presenter is asked, later, whether what it DISPLAYS is what the session
|
|
127
|
-
* SENT (`replay_draw_compare.compare_draws`), and that comparison had no second
|
|
128
|
-
* side: the live document keeps the frame's identity and object table but drops
|
|
129
|
-
* every geometry payload the moment it has built the `BufferGeometry` (850 MB of
|
|
130
|
-
* main-thread heap otherwise — `blender-runtime-view.ts`), and nothing else
|
|
131
|
-
* remembered the frame at all.
|
|
132
|
-
*
|
|
133
|
-
* So the worker records it here, while the arena is still the frame's own: every
|
|
134
|
-
* `{offset, length, dtype, count, stride}` becomes `{dtype, length, sha256}` and
|
|
135
|
-
* everything else — objects, materials, cameras, the world, the counts — is
|
|
136
|
-
* copied as it stands. THE COLUMN BYTES NEVER LEAVE THE TAB; a record of the
|
|
137
|
-
* numbers is not the numbers, and a battery run must not write a scene's meshes
|
|
138
|
-
* to disk a second time.
|
|
139
|
-
*
|
|
140
|
-
* This runs BEFORE `presentToTab` posts, for two reasons that are both fatal
|
|
141
|
-
* otherwise: the buffers are TRANSFERRED (detached the instant they are posted),
|
|
142
|
-
* and the arena is overwritten by the next `export_frame`.
|
|
143
|
-
*/
|
|
124
|
+
/** Record the submitted frame’s metadata and hash each arena column before presentToTab transfers its buffers. Geometry bytes stay in the tab; the record identifies the data that was submitted. */
|
|
144
125
|
export async function describeFrame(arena: Uint8Array, frame: unknown): Promise<unknown> {
|
|
145
126
|
const walk = async (value: unknown, key: string): Promise<unknown> => {
|
|
146
127
|
if (isColumnDescriptor(value)) {
|