From 95480a231c9f3404130207a31809a7235a242002 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Feb 2026 18:00:01 +0500 Subject: [PATCH] Move handling legacy mathutils buffer types to separate methods --- .../bonsai/bim/module/drawing/decoration.py | 32 ++++++++----------- src/bonsai/bonsai/tool/blender.py | 31 +++++++++++++++++- src/bonsai/bonsai/tool/geometry.py | 5 ++- src/bonsai/bonsai/tool/ifc.py | 21 ++++-------- src/bonsai/test/tool/test_geometry.py | 4 +-- 5 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 0a6bc967bf..705fabfcf9 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1762,40 +1762,34 @@ class CutDecorator: shader.uniform_float("color", color) batch.draw(shader) - def cache_camera_matrix(self): + def cache_camera_matrix(self) -> None: + assert bpy.context.scene and bpy.context.scene.camera obj = bpy.context.scene.camera - # Explicit `dtype` for Blender <5.0 compatibility. DecoratorData.camera_location_checksum = repr( - np.array(obj.matrix_world.translation, dtype=np.float32).tobytes() + tool.Blender.np_array_legacy(obj.matrix_world.translation).tobytes() ) - DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes()) + DecoratorData.camera_rotation_checksum = repr(tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()).tobytes()) - def is_camera_moved(self): + def is_camera_moved(self) -> bool: if not DecoratorData.camera_location_checksum: self.cache_camera_matrix() return True # Let's be conservative + + assert bpy.context.scene and bpy.context.scene.camera obj = bpy.context.scene.camera # Handle both old float64 and new float32 checksums for version compatibility - loc_checksum_bytes = eval(DecoratorData.camera_location_checksum) - if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64) - loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32) - else: # New format: 3 * 4 bytes (float32) - loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32) - - loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten() + loc_checksum_bytes: bytes = eval(DecoratorData.camera_location_checksum) + loc_check = tool.Blender.np_frombuffer_legacy(loc_checksum_bytes, 3) + loc_real = tool.Blender.np_array_legacy(obj.matrix_world.translation) if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm self.cache_camera_matrix() return True # Handle both old float64 and new float32 checksums for version compatibility - rot_checksum_bytes = eval(DecoratorData.camera_rotation_checksum) - if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64) - rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3) - else: # New format: 9 * 4 bytes (float32) - rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3) - - rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32) + rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum) + rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9) + rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()) rot_dot = np.dot(rot_check, rot_real.T) angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1)) if angle_rad > 0.0017453292519943296: # 0.1 degrees diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index d4491e104a..b78772fbc5 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -49,7 +49,7 @@ import ifcopenshell.util.element import numpy as np import numpy.typing as npt from ifcopenshell import entity_instance -from mathutils import Vector +from mathutils import Matrix, Vector import bonsai.bim import bonsai.core.tool @@ -2127,3 +2127,32 @@ class Blender(bonsai.core.tool.Blender): if cls.BLENDER_5: return "BLENDER_EEVEE" return "BLENDER_EEVEE_NEXT" + + @classmethod + def np_frombuffer_legacy(cls, bytedata: bytes, n: int) -> npt.NDArray[np.float32]: + """ + Read ``n`` float values from ``bytedata``, regardless if they are stored as ``float32`` or ``float64``. + Needed to support .blend files saved in Blender <5.0.0. + Also allows to work with .blend files from 5.0.0+ in older Blender versions. + + In ``bpy.app.version >= 5.0.0`` ``mathutils`` transitioned to use ``float32`` buffer type, + while in previous version they were using ``float64``. + In some cases we are storing raw bytes (e.g. object transforms cheksums), so old .blend files + might still have ``float64`` data stored. + + See https://projects.blender.org/blender/blender/issues/149283 + """ + if len(bytedata) == (n * 2): + return np.frombuffer(bytedata, dtype=np.float64).astype(np.float32) + return np.frombuffer(bytedata, dtype=np.float32) + + @classmethod + def np_array_legacy(cls, mathutils_type: Union[Vector, Matrix]) -> npt.NDArray[np.float32]: + """ + Converts ``mathutils`` types to ``np.float32`` arrays, regardless of Blender version. + + See ``np_frombuffer_legacy`` for more details. + """ + if cls.BLENDER_5: + return np.array(mathutils_type) + return np.array(mathutils_type, dtype=np.float32) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 5a6429adfe..13ec6695d2 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1152,9 +1152,8 @@ class Geometry(bonsai.core.tool.Geometry): def record_object_position(cls, obj: bpy.types.Object) -> None: # These are recorded separately because they have different numerical tolerances props = tool.Blender.get_object_bim_props(obj) - # Explicit dtype for Blender <5.0 compatibility. - props.location_checksum = repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()) - props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes()) + props.location_checksum = repr(tool.Blender.np_array_legacy(obj.matrix_world.translation).tobytes()) + props.rotation_checksum = repr(tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()).tobytes()) @classmethod def remove_connection(cls, connection: ifcopenshell.entity_instance) -> None: diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 81c5202c19..8cbd2a112b 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -115,24 +115,17 @@ class Ifc(bonsai.core.tool.Ifc): return True # Let's be conservative # Handle both old float64 and new float32 checksums for version compatibility - loc_checksum_bytes = eval(oprops.location_checksum) - if len(loc_checksum_bytes) == 24: # Old format: 3 * 8 bytes (float64) - loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float64).astype(np.float32) - else: # New format: 3 * 4 bytes (float32) - loc_check = np.frombuffer(loc_checksum_bytes, dtype=np.float32) - - loc_real = np.array(obj.matrix_world.translation, dtype=np.float32).flatten() + loc_checksum_bytes: bytes = eval(oprops.location_checksum) + loc_check = tool.Blender.np_frombuffer_legacy(loc_checksum_bytes, 3) + loc_real = tool.Blender.np_array_legacy(obj.matrix_world.translation) if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm return True # Handle both old float64 and new float32 checksums for version compatibility - rot_checksum_bytes = eval(oprops.rotation_checksum) - if len(rot_checksum_bytes) == 72: # Old format: 9 * 8 bytes (float64) - rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float64).astype(np.float32).reshape(3, 3) - else: # New format: 9 * 4 bytes (float32) - rot_check = np.frombuffer(rot_checksum_bytes, dtype=np.float32).reshape(3, 3) - - rot_real = np.array(obj.matrix_world.to_3x3(), dtype=np.float32) + rot_checksum_bytes: bytes = eval(oprops.rotation_checksum) + rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9) + rot_check = rot_check.reshape(3, 3) + rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()) rot_dot = np.dot(rot_check, rot_real.T) angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1)) if angle_rad > 0.0017453292519943296: # 0.1 degrees diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index 34300f2405..424e1fd876 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -323,8 +323,8 @@ class TestRecordObjectPosition(NewFile): obj = bpy.data.objects.new("Object", None) props = tool.Blender.get_object_bim_props(obj) subject.record_object_position(obj) - assert props.location_checksum == repr(np.array(obj.matrix_world.translation, dtype=np.float32).tobytes()) - assert props.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3(), dtype=np.float32).tobytes()) + assert props.location_checksum == repr(tool.Blender.np_array_legacy(obj.matrix_world.translation).tobytes()) + assert props.rotation_checksum == repr(tool.Blender.np_array_legacy(obj.matrix_world.to_3x3()).tobytes()) class TestRemoveConnection(NewFile):