@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
# SPDX-FileCopyrightText: 2018 iCyP
|
|
3
|
+
|
|
4
|
+
from . import registration
|
|
5
|
+
from .exporter import gltf2_export_user_extension
|
|
6
|
+
from .importer import gltf2_import_user_extension
|
|
7
|
+
|
|
8
|
+
bl_info = {
|
|
9
|
+
"name": "VRM format",
|
|
10
|
+
"author": "saturday06, iCyP",
|
|
11
|
+
"version": (
|
|
12
|
+
4, # x-release-please-major
|
|
13
|
+
10, # x-release-please-minor
|
|
14
|
+
3, # x-release-please-patch
|
|
15
|
+
),
|
|
16
|
+
"location": "File > Import-Export",
|
|
17
|
+
"description": "Import-Edit-Export VRM",
|
|
18
|
+
"blender": (2, 93, 0),
|
|
19
|
+
"warning": "",
|
|
20
|
+
"support": "COMMUNITY",
|
|
21
|
+
"wiki_url": "",
|
|
22
|
+
"doc_url": "https://vrm-addon-for-blender.info",
|
|
23
|
+
"tracker_url": "https://github.com/saturday06/VRM-Addon-for-Blender/issues",
|
|
24
|
+
"category": "Import-Export",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register() -> None:
|
|
29
|
+
registration.register()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def unregister() -> None:
|
|
33
|
+
registration.unregister()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class glTF2ImportUserExtension(gltf2_import_user_extension.glTF2ImportUserExtension):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class glTF2ExportUserExtension(gltf2_export_user_extension.glTF2ExportUserExtension):
|
|
41
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
schema_version = "1.0.0"
|
|
2
|
+
id = "vrm"
|
|
3
|
+
version = "4.10.3"
|
|
4
|
+
name = "VRM format"
|
|
5
|
+
tagline = "VRM import, export and editing capabilities"
|
|
6
|
+
maintainer = "Isamu Mogi <dyob@lunaport.net>"
|
|
7
|
+
type = "add-on"
|
|
8
|
+
website = "https://vrm-addon-for-blender.info"
|
|
9
|
+
tags = ["Animation", "Import-Export", "Material", "Modeling", "Physics"]
|
|
10
|
+
blender_version_min = "4.2.0"
|
|
11
|
+
blender_version_max = "5.3.0"
|
|
12
|
+
license = ["SPDX:MIT", "SPDX:GPL-3.0-or-later"]
|
|
13
|
+
copyright = ["2020 Isamu Mogi", "2018 iCyP"]
|
|
14
|
+
|
|
15
|
+
[permissions]
|
|
16
|
+
files = "Import/export VRM from/to disk"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from bpy.app.handlers import persistent
|
|
7
|
+
from bpy.types import Context
|
|
8
|
+
|
|
9
|
+
from .logger import get_logger
|
|
10
|
+
|
|
11
|
+
_logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class State:
|
|
16
|
+
during_animation_playback: bool = False
|
|
17
|
+
during_frame_change: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_state: Final = State()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_animation_playing(context: Context) -> bool:
|
|
24
|
+
if _state.during_animation_playback:
|
|
25
|
+
return True
|
|
26
|
+
screen = context.screen
|
|
27
|
+
if not screen:
|
|
28
|
+
return False
|
|
29
|
+
return screen.is_animation_playing
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def defer_shape_key_update(context: Context) -> bool:
|
|
33
|
+
return _is_animation_playing(context) or _state.during_frame_change
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@persistent
|
|
37
|
+
def animation_playback_pre(_unused: object) -> None:
|
|
38
|
+
_state.during_animation_playback = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@persistent
|
|
42
|
+
def animation_playback_post(_unused: object) -> None:
|
|
43
|
+
_state.during_animation_playback = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@persistent
|
|
47
|
+
def frame_change_pre(_unused: object) -> None:
|
|
48
|
+
_state.during_frame_change = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@persistent
|
|
52
|
+
def frame_change_post(_unused: object) -> None:
|
|
53
|
+
_state.during_frame_change = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clear_global_variables() -> None:
|
|
57
|
+
_state.during_animation_playback = False
|
|
58
|
+
_state.during_frame_change = False
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class BlenderManifest:
|
|
10
|
+
id: str
|
|
11
|
+
version: tuple[int, int, int]
|
|
12
|
+
blender_version_min: tuple[int, int, int]
|
|
13
|
+
blender_version_max: Optional[tuple[int, int, int]]
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def default_blender_manifest_path(cls) -> Path:
|
|
17
|
+
return Path(__file__).parent.parent / "blender_manifest.toml"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def _read_str(cls, blender_manifest: str, key: str) -> Optional[str]:
|
|
21
|
+
# When the version of Python used by the minimum supported version of Blender
|
|
22
|
+
# exceeds 3.11, it is rewritten in the tomllib library.
|
|
23
|
+
lines = [line.strip() for line in blender_manifest.splitlines()]
|
|
24
|
+
|
|
25
|
+
pattern = key + r' = "([^"]+)"'
|
|
26
|
+
for line in lines:
|
|
27
|
+
match = re.fullmatch(pattern, line)
|
|
28
|
+
if not match:
|
|
29
|
+
continue
|
|
30
|
+
return match[1]
|
|
31
|
+
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _read_3_tuple_version(
|
|
36
|
+
cls, blender_manifest: str, key: str
|
|
37
|
+
) -> Optional[tuple[int, int, int]]:
|
|
38
|
+
# When the version of Python used by the minimum supported version of Blender
|
|
39
|
+
# exceeds 3.11, it is rewritten in the tomli library.
|
|
40
|
+
lines = [line.strip() for line in blender_manifest.splitlines()]
|
|
41
|
+
|
|
42
|
+
pattern = key + r' = "(\d+)\.(\d+)\.(\d+)"'
|
|
43
|
+
for line in lines:
|
|
44
|
+
match = re.fullmatch(pattern, line)
|
|
45
|
+
if not match:
|
|
46
|
+
continue
|
|
47
|
+
return (int(match[1]), int(match[2]), int(match[3]))
|
|
48
|
+
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def read(cls, blender_manifest: Optional[str] = None) -> "BlenderManifest":
|
|
53
|
+
if blender_manifest is None:
|
|
54
|
+
blender_manifest = cls.default_blender_manifest_path().read_text(
|
|
55
|
+
encoding="UTF-8"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
addon_id = cls._read_str(blender_manifest, "id")
|
|
59
|
+
if addon_id is None:
|
|
60
|
+
message = "'id' was not found in blender manifest"
|
|
61
|
+
raise ValueError(message)
|
|
62
|
+
|
|
63
|
+
version = cls._read_3_tuple_version(blender_manifest, "version")
|
|
64
|
+
if version is None:
|
|
65
|
+
message = "'version' was not found in blender manifest"
|
|
66
|
+
raise ValueError(message)
|
|
67
|
+
|
|
68
|
+
blender_version_min = cls._read_3_tuple_version(
|
|
69
|
+
blender_manifest, "blender_version_min"
|
|
70
|
+
)
|
|
71
|
+
if blender_version_min is None:
|
|
72
|
+
message = "'blender_version_min' was not found in blender manifest"
|
|
73
|
+
raise ValueError(message)
|
|
74
|
+
|
|
75
|
+
return BlenderManifest(
|
|
76
|
+
id=addon_id,
|
|
77
|
+
version=version,
|
|
78
|
+
blender_version_min=blender_version_min,
|
|
79
|
+
blender_version_max=cls._read_3_tuple_version(
|
|
80
|
+
blender_manifest, "blender_version_max"
|
|
81
|
+
),
|
|
82
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
INTERNAL_NAME_PREFIX: Final = "\N{FULLWIDTH BROKEN BAR}"
|
|
6
|
+
"""
|
|
7
|
+
The letter to be prefixed when naming internal invisible objects. It was chosen so that
|
|
8
|
+
the sort order would be backward and also so that it would be easy to identify the
|
|
9
|
+
object as internal.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
DISABLE_TRANSLATION: Final = "\N{ZERO WIDTH SPACE}"
|
|
13
|
+
|
|
14
|
+
FULLWIDTH_ASCII_TO_ASCII_MAP: Final[Mapping[str, str]] = {
|
|
15
|
+
chr(ascii_char + 0x0000_FEE0): chr(ascii_char) for ascii_char in range(0x21, 0x7E)
|
|
16
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
import math
|
|
3
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
4
|
+
from sys import float_info
|
|
5
|
+
from typing import Final, Optional, Union
|
|
6
|
+
|
|
7
|
+
from . import convert_any
|
|
8
|
+
from .logger import get_logger
|
|
9
|
+
|
|
10
|
+
_logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
BPY_TRACK_AXIS_TO_VRM_AIM_AXIS: Final[Mapping[str, str]] = {
|
|
13
|
+
"TRACK_X": "PositiveX",
|
|
14
|
+
"TRACK_Y": "PositiveY",
|
|
15
|
+
"TRACK_Z": "PositiveZ",
|
|
16
|
+
"TRACK_NEGATIVE_X": "NegativeX",
|
|
17
|
+
"TRACK_NEGATIVE_Y": "NegativeY",
|
|
18
|
+
"TRACK_NEGATIVE_Z": "NegativeZ",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
VRM_AIM_AXIS_TO_BPY_TRACK_AXIS: Final[Mapping[str, str]] = {
|
|
22
|
+
v: k for k, v in BPY_TRACK_AXIS_TO_VRM_AIM_AXIS.items()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Json = Union[
|
|
27
|
+
None,
|
|
28
|
+
bool,
|
|
29
|
+
int,
|
|
30
|
+
float,
|
|
31
|
+
str,
|
|
32
|
+
list["Json"],
|
|
33
|
+
dict[str, "Json"],
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
JsonView = Union[
|
|
37
|
+
None,
|
|
38
|
+
bool,
|
|
39
|
+
int,
|
|
40
|
+
float,
|
|
41
|
+
str,
|
|
42
|
+
Sequence["JsonView"],
|
|
43
|
+
Mapping[str, "JsonView"],
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def iterator_or_none(v: object) -> Optional[Iterator[object]]:
|
|
48
|
+
try:
|
|
49
|
+
# "isinstance(v, Iterable)" doesn't work.
|
|
50
|
+
# https://github.com/python/cpython/blob/3.9/Doc/library/collections.abc.rst?plain=1#L126-L127
|
|
51
|
+
iterator = iter(v) # type: ignore[call-overload]
|
|
52
|
+
except TypeError:
|
|
53
|
+
return None
|
|
54
|
+
return convert_any.iterator_to_object_iterator(iterator)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def sequence_or_none(
|
|
58
|
+
sequence_object: object,
|
|
59
|
+
) -> Optional[Sequence[object]]:
|
|
60
|
+
iterator = iterator_or_none(sequence_object)
|
|
61
|
+
if iterator is None:
|
|
62
|
+
return None
|
|
63
|
+
return list(iterator)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def mapping_or_none(
|
|
67
|
+
mapping_object: object,
|
|
68
|
+
) -> Optional[Mapping[object, object]]:
|
|
69
|
+
return convert_any.mapping_to_object_mapping(mapping_object)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def vrm_json_vector3_to_tuple(
|
|
73
|
+
value: Json,
|
|
74
|
+
) -> Optional[tuple[float, float, float]]:
|
|
75
|
+
if not isinstance(value, dict):
|
|
76
|
+
return None
|
|
77
|
+
x = float_or(value.get("x"), 0.0)
|
|
78
|
+
y = float_or(value.get("y"), 0.0)
|
|
79
|
+
z = float_or(value.get("z"), 0.0)
|
|
80
|
+
return (x, y, z)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def vrm_json_curve_to_list(curve: Json) -> Optional[list[float]]:
|
|
84
|
+
if not isinstance(curve, list):
|
|
85
|
+
return None
|
|
86
|
+
values: list[float] = [float_or(v, 0.0) for v in curve]
|
|
87
|
+
while len(values) < 8:
|
|
88
|
+
values.append(0)
|
|
89
|
+
while len(values) > 8:
|
|
90
|
+
values.pop()
|
|
91
|
+
return values
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def vrm_json_array_to_float_vector(
|
|
95
|
+
input_values: Json, defaults: list[float]
|
|
96
|
+
) -> list[float]:
|
|
97
|
+
if not isinstance(input_values, list):
|
|
98
|
+
return defaults
|
|
99
|
+
|
|
100
|
+
output_values: list[float] = []
|
|
101
|
+
for index, default in enumerate(defaults):
|
|
102
|
+
if index >= len(input_values):
|
|
103
|
+
output_values.append(default)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
input_value = float_or(input_values[index], default)
|
|
107
|
+
output_values.append(input_value)
|
|
108
|
+
|
|
109
|
+
return output_values
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def mtoon_shading_toony_1_to_0(shading_toony: float, shading_shift: float) -> float:
|
|
113
|
+
base = 2 - shading_toony + shading_shift
|
|
114
|
+
if abs(base) < float_info.epsilon:
|
|
115
|
+
# https://github.com/Santarh/MToon/blob/43f02fe8c9b19c3cf9f3238003b7d8b332933833/MToon/Resources/Shaders/MToon.shader#L17
|
|
116
|
+
return 0.9
|
|
117
|
+
return max(0, min((shading_toony + shading_shift) / base, 1))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def mtoon_shading_shift_1_to_0(shading_toony: float, shading_shift: float) -> float:
|
|
121
|
+
return max(-1, min(shading_toony - shading_shift - 1, 1))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def mtoon_gi_equalization_to_intensity(gi_equalization: float) -> float:
|
|
125
|
+
return max(0, min(1.0, 1.0 - gi_equalization))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L10-L19
|
|
129
|
+
def mtoon_shading_toony_0_to_1(
|
|
130
|
+
shading_toony_0x: float, shading_shift_0x: float
|
|
131
|
+
) -> float:
|
|
132
|
+
(range_min, range_max) = _get_shading_range_0x(shading_toony_0x, shading_shift_0x)
|
|
133
|
+
return max(0, min(1.0, (2.0 - (range_max - range_min)) * 0.5))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L21-L30
|
|
137
|
+
def mtoon_shading_shift_0_to_1(
|
|
138
|
+
shading_toony_0x: float, shading_shift_0x: float
|
|
139
|
+
) -> float:
|
|
140
|
+
(range_min, range_max) = _get_shading_range_0x(shading_toony_0x, shading_shift_0x)
|
|
141
|
+
return max(-1, min(1.0, ((range_max + range_min) * 0.5 * -1)))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L32-L38
|
|
145
|
+
def mtoon_intensity_to_gi_equalization(gi_intensity_0x: float) -> float:
|
|
146
|
+
return max(0, min(1.0, 1.0 - gi_intensity_0x))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRMShaders/VRM10/MToon10/Runtime/MToon10Migrator.cs#L40-L46
|
|
150
|
+
def _get_shading_range_0x(
|
|
151
|
+
shading_toony_0x: float, shading_shift_0x: float
|
|
152
|
+
) -> tuple[float, float]:
|
|
153
|
+
range_min = shading_shift_0x
|
|
154
|
+
range_max = (1 - shading_toony_0x) + shading_toony_0x * shading_shift_0x
|
|
155
|
+
return (range_min, range_max)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def float_or_none(
|
|
159
|
+
v: object,
|
|
160
|
+
min_value: float = -float_info.max,
|
|
161
|
+
max_value: float = float_info.max,
|
|
162
|
+
) -> Optional[float]:
|
|
163
|
+
if isinstance(v, float):
|
|
164
|
+
if math.isnan(v):
|
|
165
|
+
return None
|
|
166
|
+
return max(min_value, min(v, max_value))
|
|
167
|
+
|
|
168
|
+
if isinstance(v, bool):
|
|
169
|
+
return max(min_value, min(1.0 if v else 0.0, max_value))
|
|
170
|
+
|
|
171
|
+
if isinstance(v, int):
|
|
172
|
+
return max(min_value, min(float(v), max_value))
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def float_or(
|
|
178
|
+
v: object,
|
|
179
|
+
default: float,
|
|
180
|
+
min_value: float = -float_info.max,
|
|
181
|
+
max_value: float = float_info.max,
|
|
182
|
+
) -> float:
|
|
183
|
+
float_or_none_value = float_or_none(v, min_value, max_value)
|
|
184
|
+
if float_or_none_value is not None:
|
|
185
|
+
return float_or_none_value
|
|
186
|
+
|
|
187
|
+
if min_value <= default <= max_value:
|
|
188
|
+
return default
|
|
189
|
+
|
|
190
|
+
_logger.warning(
|
|
191
|
+
"Default float value %s is out of range [%s, %s]", default, min_value, max_value
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return max(min_value, min(default, max_value))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def float4_or_none(value4: object) -> Optional[tuple[float, float, float, float]]:
|
|
198
|
+
value4_iterator = iterator_or_none(value4)
|
|
199
|
+
if value4_iterator is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
result: list[float] = []
|
|
203
|
+
for value in value4_iterator:
|
|
204
|
+
if len(result) == 4:
|
|
205
|
+
return None
|
|
206
|
+
float_value = float_or_none(value)
|
|
207
|
+
if float_value is None:
|
|
208
|
+
return None
|
|
209
|
+
result.append(float_value)
|
|
210
|
+
if len(result) != 4:
|
|
211
|
+
return None
|
|
212
|
+
return (result[0], result[1], result[2], result[3])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def float4_or(
|
|
216
|
+
value4: object, default: tuple[float, float, float, float]
|
|
217
|
+
) -> tuple[float, float, float, float]:
|
|
218
|
+
float4 = float4_or_none(value4)
|
|
219
|
+
return float4 if float4 is not None else default
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def float3_or_none(value3: object) -> Optional[tuple[float, float, float]]:
|
|
223
|
+
value3_iterator = iterator_or_none(value3)
|
|
224
|
+
if value3_iterator is None:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
result: list[float] = []
|
|
228
|
+
for value in value3_iterator:
|
|
229
|
+
if len(result) == 3:
|
|
230
|
+
return None
|
|
231
|
+
float_value = float_or_none(value)
|
|
232
|
+
if float_value is None:
|
|
233
|
+
return None
|
|
234
|
+
result.append(float_value)
|
|
235
|
+
if len(result) != 3:
|
|
236
|
+
return None
|
|
237
|
+
return (result[0], result[1], result[2])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def float3_or(
|
|
241
|
+
value3: object, default: tuple[float, float, float]
|
|
242
|
+
) -> tuple[float, float, float]:
|
|
243
|
+
float3 = float3_or_none(value3)
|
|
244
|
+
return float3 if float3 is not None else default
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def float2_or_none(value2: object) -> Optional[tuple[float, float]]:
|
|
248
|
+
value2_iterator = iterator_or_none(value2)
|
|
249
|
+
if value2_iterator is None:
|
|
250
|
+
return None
|
|
251
|
+
result: list[float] = []
|
|
252
|
+
for value in value2_iterator:
|
|
253
|
+
if len(result) == 2:
|
|
254
|
+
return None
|
|
255
|
+
float_value = float_or_none(value)
|
|
256
|
+
if float_value is None:
|
|
257
|
+
return None
|
|
258
|
+
result.append(float_value)
|
|
259
|
+
if len(result) != 2:
|
|
260
|
+
return None
|
|
261
|
+
return (result[0], result[1])
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def axis_blender_to_gltf(vector3: Sequence[float]) -> tuple[float, float, float]:
|
|
265
|
+
return (
|
|
266
|
+
-vector3[0],
|
|
267
|
+
vector3[2],
|
|
268
|
+
vector3[1],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def linear_to_srgb(
|
|
273
|
+
non_color: Sequence[float],
|
|
274
|
+
) -> Sequence[float]:
|
|
275
|
+
return [
|
|
276
|
+
math.pow(channel_value, 1.0 / 2.2) if channel_index < 3 else channel_value
|
|
277
|
+
for channel_index, channel_value in enumerate(non_color)
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def srgb_to_linear(
|
|
282
|
+
srgb_color: Sequence[float],
|
|
283
|
+
) -> Sequence[float]:
|
|
284
|
+
return [
|
|
285
|
+
math.pow(channel_value, 2.2) if channel_index < 3 else channel_value
|
|
286
|
+
for channel_index, channel_value in enumerate(srgb_color)
|
|
287
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
"""A module provides a function to convert a variable of type Any to a concrete type.
|
|
3
|
+
|
|
4
|
+
Inevitably, variables of type Any may occur, and such variables cannot be handled
|
|
5
|
+
in type checkers in strict mode. Any is allowed only in the module here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Iterator, Mapping
|
|
10
|
+
from typing import (
|
|
11
|
+
Any, # Any is allowed only in the module here.
|
|
12
|
+
Optional,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_object( # type: ignore[explicit-any]
|
|
17
|
+
any_object: Any, # noqa: ANN401 # Any is allowed only in the module here.
|
|
18
|
+
) -> object:
|
|
19
|
+
# Interpret Unknown as object
|
|
20
|
+
# https://github.com/microsoft/pyright/issues/3650
|
|
21
|
+
if not isinstance(any_object, object):
|
|
22
|
+
if sys.version_info >= (3, 11):
|
|
23
|
+
from typing import assert_never
|
|
24
|
+
|
|
25
|
+
assert_never(any_object)
|
|
26
|
+
raise TypeError
|
|
27
|
+
return any_object
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def iterator_to_object_iterator( # type: ignore[explicit-any]
|
|
31
|
+
any_iterator: Any, # noqa: ANN401 # Any is allowed only in the module here.
|
|
32
|
+
) -> Optional[Iterator[object]]:
|
|
33
|
+
any_iterator_without_partial_type_narrowing = any_iterator
|
|
34
|
+
if not isinstance(any_iterator, Iterator):
|
|
35
|
+
return None
|
|
36
|
+
return map(to_object, any_iterator_without_partial_type_narrowing)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mapping_to_object_mapping( # type: ignore[explicit-any]
|
|
40
|
+
any_mapping: Any, # noqa: ANN401 # Any is allowed only in the module here.
|
|
41
|
+
) -> Optional[Mapping[object, object]]:
|
|
42
|
+
any_mapping_without_partial_type_narrowing = any_mapping
|
|
43
|
+
if not isinstance(any_mapping, Mapping):
|
|
44
|
+
return None
|
|
45
|
+
return {
|
|
46
|
+
to_object(k): to_object(v)
|
|
47
|
+
for k, v in any_mapping_without_partial_type_narrowing.items()
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
2
|
+
import math
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from mathutils import Matrix, Quaternion, Vector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def dump(v: Union[Matrix, Vector, Quaternion, float]) -> str:
|
|
10
|
+
if isinstance(v, (float, int)):
|
|
11
|
+
return str(v)
|
|
12
|
+
|
|
13
|
+
if isinstance(v, Matrix):
|
|
14
|
+
t, r, s = v.decompose()
|
|
15
|
+
return f"Matrix(T={dump(t)},R={dump(r)},S={dump(s)})"
|
|
16
|
+
|
|
17
|
+
if isinstance(v, Vector):
|
|
18
|
+
return f"({v.x:.3f},{v.y:.3f},{v.z:.3f})"
|
|
19
|
+
|
|
20
|
+
x, y, z = (round(math.degrees(xyz)) for xyz in v.to_euler()[:])
|
|
21
|
+
return f"Euler({x},{y},{z})"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def assert_vector3_equals(
|
|
25
|
+
expected: Vector, actual: Sequence[float], message: str
|
|
26
|
+
) -> None:
|
|
27
|
+
if len(actual) != 3:
|
|
28
|
+
message = f"actual length is not 3: {actual}"
|
|
29
|
+
raise AssertionError(message)
|
|
30
|
+
|
|
31
|
+
threshold = 0.0001
|
|
32
|
+
if abs(expected[0] - actual[0]) > threshold:
|
|
33
|
+
message = f"{message}: {tuple(expected)} is different from {tuple(actual)}"
|
|
34
|
+
raise AssertionError(message)
|
|
35
|
+
if abs(expected[1] - actual[1]) > threshold:
|
|
36
|
+
message = f"{message}: {tuple(expected)} is different from {tuple(actual)}"
|
|
37
|
+
raise AssertionError(message)
|
|
38
|
+
if abs(expected[2] - actual[2]) > threshold:
|
|
39
|
+
message = f"{message}: {tuple(expected)} is different from {tuple(actual)}"
|
|
40
|
+
raise AssertionError(message)
|