From 16de56d9daef97baa76847e19450f04e317add3c Mon Sep 17 00:00:00 2001 From: zvodd Date: Thu, 5 Feb 2026 14:25:00 +0800 Subject: [PATCH 1/2] Fix UNDO related invalid object references causing blender to crash - Place Tool (TODO other tools). Fix intermited rotation loss when repicking objects - Place Tool --- place_tool/_runtime.py | 12 ++++----- place_tool/draw_bbox.py | 8 +++--- place_tool/gzg.py | 57 +++++++++++++++++++-------------------- place_tool/op.py | 60 ++++++++++++++++++++++++++++++----------- 4 files changed, 82 insertions(+), 55 deletions(-) diff --git a/place_tool/_runtime.py b/place_tool/_runtime.py index 9597cc2..a94b5a6 100644 --- a/place_tool/_runtime.py +++ b/place_tool/_runtime.py @@ -1,22 +1,22 @@ # -# 使用 bpy.types.Object 为key,返回以该物体构建的AlignObject -# bpy.types.Object : AlignObject +# 使用 bpy.types.Object.name 为key,返回以该物体构建的AlignObject +# str : AlignObject # 运行时缓存 -# 结构:bpy.types.Object : AlignObject +# 结构: bpy.types.Object.name : AlignObject # ------------------------------------------------------------ # 存放预计算的场景物体 SCENE_OBJS = {} # 存放实时更新的激活项物体 -ALIGN_OBJ = {'active': None, - 'active_prv': None} +ALIGN_OBJ = {'active_name': None, + 'active_prv_name': None} ALIGN_OBJS = {'bbox_pts': None, 'center': None, 'top': None, 'bottom': None} -OVERLAP_OBJ = {'obj': None, +OVERLAP_OBJ = {'obj_name': None, 'is_project': False} diff --git a/place_tool/draw_bbox.py b/place_tool/draw_bbox.py index afbda5d..f4575a0 100644 --- a/place_tool/draw_bbox.py +++ b/place_tool/draw_bbox.py @@ -9,7 +9,7 @@ from gpu_extras.presets import draw_circle_2d from mathutils import Vector, Matrix -from ._runtime import ALIGN_OBJ, OVERLAP_OBJ, ALIGN_OBJS +from ._runtime import ALIGN_OBJ, OVERLAP_OBJ, ALIGN_OBJS, SCENE_OBJS from ..utils import get_pref from ..utils.obj_bbox import AlignObject @@ -37,7 +37,8 @@ def draw_bbox_callback(self, context): if not context.object: return - overlap_obj_a = OVERLAP_OBJ.get('obj') + overlap_name = OVERLAP_OBJ.get('obj_name') + overlap_obj_a = SCENE_OBJS.get(overlap_name) if overlap_name else None pref_bbox = get_pref().place_tool.bbox width = pref_bbox.width @@ -48,7 +49,8 @@ def draw_bbox_callback(self, context): with wrap_bgl_restore(width): if context.object and len(context.selected_objects) == 1: - obj_A = ALIGN_OBJ.get('active', None) + active_name = ALIGN_OBJ.get('active_name') + obj_A = SCENE_OBJS.get(active_name) if active_name else None if context.object.type in C_OBJECT_TYPE and obj_A: # mesh object shader_2d = get_shader('2d') diff --git a/place_tool/gzg.py b/place_tool/gzg.py index e1e875a..5b8eac7 100644 --- a/place_tool/gzg.py +++ b/place_tool/gzg.py @@ -3,7 +3,7 @@ import bpy from mathutils import Vector, Matrix -from ._runtime import ALIGN_OBJ, ALIGN_OBJS +from ._runtime import ALIGN_OBJ, ALIGN_OBJS, SCENE_OBJS from ..utils import get_pref from ..utils.get_gz_matrix import local_matrix from ..utils.get_position import get_objs_bbox_top @@ -103,29 +103,26 @@ def correct_gz_loc(self, context): self.rotate_gz.matrix_basis = context.object.matrix_world.normalized() self.scale_gz.matrix_basis = context.object.matrix_world.normalized() - obj_A = ALIGN_OBJ.get("active") + active_name = ALIGN_OBJ.get("active_name") + obj_A = SCENE_OBJS.get(active_name) if active_name else None if obj_A and len(context.selected_objects) == 1: - try: - x, y, z, xD, yD, zD = local_matrix(reverse_zD=True) - axis = context.scene.place_tool.axis - invert = context.scene.place_tool.invert_axis - if axis == "X": - q = x if not invert else xD - elif axis == "Y": - q = y if not invert else yD - elif axis == "Z": - q = z if not invert else zD - - pos = obj_A.get_axis_center(axis, not invert, is_local=False) - scale = Vector((1, 1, 1)) - mx = Matrix.LocRotScale(pos, q, scale) - - self.rotate_gz.matrix_basis = mx - self.scale_gz.matrix_basis = mx - except ReferenceError as e: - print(e.args) - pass + x, y, z, xD, yD, zD = local_matrix(reverse_zD=True) + axis = context.scene.place_tool.axis + invert = context.scene.place_tool.invert_axis + if axis == "X": + q = x if not invert else xD + elif axis == "Y": + q = y if not invert else yD + elif axis == "Z": + q = z if not invert else zD + + pos = obj_A.get_axis_center(axis, not invert, is_local=False) + scale = Vector((1, 1, 1)) + mx = Matrix.LocRotScale(pos, q, scale) + + self.rotate_gz.matrix_basis = mx + self.scale_gz.matrix_basis = mx elif obj_A and len(context.selected_objects) > 1: try: @@ -144,8 +141,10 @@ def correct_gz_loc(self, context): elif not obj_A or obj_A.obj != context.object: if context.object.type in {"MESH", "CURVE", "SURFACE", "FONT", "LIGHT"}: - ALIGN_OBJ["active"] = AlignObject(context.object, - "ACCURATE", True) + align_obj = AlignObject(context.object, "ACCURATE", True) + SCENE_OBJS[context.object.name] = align_obj + ALIGN_OBJ["active_name"] = context.object.name + def refresh(self, context): prop = context.scene.place_tool @@ -167,13 +166,11 @@ def invoke_prepare(self, context, gizmo): self.refresh(context) def update_set_axis_gizmo_matrix(self, context): - obj_A = ALIGN_OBJ.get("active") + active_name = ALIGN_OBJ.get("active_name") + obj_A = SCENE_OBJS.get(active_name) if active_name else None + if obj_A: - try: - pos = obj_A.get_bbox_center(is_local=False) - except ReferenceError: - # undo self.obj been removed - return + pos = obj_A.get_bbox_center(is_local=False) else: pos = context.object.matrix_world.translation diff --git a/place_tool/op.py b/place_tool/op.py index c88abf8..d0c6b90 100644 --- a/place_tool/op.py +++ b/place_tool/op.py @@ -91,10 +91,10 @@ def build_viewlayer_objs(self): if obj is context.object: obj_A = AlignObject(obj, self.build_act_obj_mode, build_instance=self.build_act_inst) - SCENE_OBJS[obj] = obj_A - ALIGN_OBJ['active'] = obj_A + SCENE_OBJS[obj.name] = obj_A + ALIGN_OBJ['active_name'] = obj.name else: - SCENE_OBJS[obj] = AlignObject(obj, self.build_scn_obj_mode, build_instance=self.build_scn_inst) + SCENE_OBJS[obj.name] = AlignObject(obj, self.build_scn_obj_mode, build_instance=self.build_scn_inst) def is_overlap(self, context, exclude_obj_list=None): obj = context.object @@ -104,17 +104,33 @@ def is_overlap(self, context, exclude_obj_list=None): elif context.object.type not in C_OBJECT_TYPE_HAS_BBOX: return # 更新激活物体 - ALIGN_OBJ['active'].bvh_tree_update() + active_name = ALIGN_OBJ.get('active_name') + if not active_name: return + + active_align_obj = SCENE_OBJS.get(active_name) + if not active_align_obj: return + + active_align_obj.bvh_tree_update() + + exclude_obj_names = [o.name for o in exclude_obj_list] if exclude_obj_list else [] + # 检测是否更新了激活物体 - for key, obj_A in SCENE_OBJS.items(): - if obj_A.obj == ALIGN_OBJ['active'].obj: + for obj_name, obj_A in SCENE_OBJS.items(): + if obj_name == active_name: + continue + + # Safely get the object from scene + scene_obj = bpy.context.scene.objects.get(obj_name) + if not scene_obj: continue - elif obj_A.obj in context.object.children_recursive: + + if scene_obj in context.object.children_recursive: continue - elif exclude_obj_list and key in exclude_obj_list: + + if obj_name in exclude_obj_names: continue - elif ALIGN_OBJ['active'].bvh_tree.overlap(obj_A.bvh_tree): - OVERLAP_OBJ['obj'] = obj_A + elif active_align_obj.bvh_tree.overlap(obj_A.bvh_tree): + OVERLAP_OBJ['obj_name'] = obj_name return True OVERLAP_OBJ.clear() @@ -122,11 +138,13 @@ def is_overlap(self, context, exclude_obj_list=None): def check_objects_overlap(self, context, exclude_obj_list=None): if not hasattr(self, 'objs_A'): return - for key, obj_A in SCENE_OBJS.items(): - if key in exclude_obj_list: + exclude_obj_names = [o.name for o in exclude_obj_list] if exclude_obj_list else [] + + for obj_name, obj_A in SCENE_OBJS.items(): + if obj_name in exclude_obj_names: continue if self.objs_A.bvh_tree.overlap(obj_A.bvh_tree): - OVERLAP_OBJ['obj'] = obj_A + OVERLAP_OBJ['obj_name'] = obj_name return True OVERLAP_OBJ.clear() @@ -662,7 +680,11 @@ def handle_obj(self, context, event): # axis = self.axis.lower() # setattr(rot, axis, getattr(rot, axis) + offset) - obj_A = ALIGN_OBJ['active'] + obj_name = ALIGN_OBJ.get('active_name') + if not obj_name: return + obj_A = SCENE_OBJS.get(obj_name) + if not obj_A: return + pivot = obj_A.get_bbox_center(is_local=False) # get rotate axis @@ -761,7 +783,10 @@ def handle_obj(self, context, event): with mouse_offset(self, event, scale=0.01, scale_shift=0.005) as (offset_x, offset_y): offset = offset_y - self.obj_A = ALIGN_OBJ['active'] + obj_name = ALIGN_OBJ.get('active_name') + if not obj_name: return + self.obj_A = SCENE_OBJS.get(obj_name) + if not self.obj_A: return offset = offset * -1 + 1 @@ -782,7 +807,10 @@ def handle_multi_obj(self, context, event): with mouse_offset(self, event, scale=0.01, scale_shift=0.005) as (offset_x, offset_y): offset = offset_y - self.obj_A = ALIGN_OBJ['active'] + obj_name = ALIGN_OBJ.get('active_name') + if not obj_name: return + self.obj_A = SCENE_OBJS.get(obj_name) + if not self.obj_A: return offset = offset * -1 + 1 # offset_mx = self.obj_A.obj.matrix_world @ self.ori_mx[self.obj_A.obj].inverted() From 77c9b17c339b9b24652d9622623af7816df8a6de Mon Sep 17 00:00:00 2001 From: zvodd Date: Sun, 8 Feb 2026 09:07:01 +0800 Subject: [PATCH 2/2] Fix: Prevent error when raycast finds no target surface - Place Tool. (Previous commit regression) --- place_tool/op.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/place_tool/op.py b/place_tool/op.py index d0c6b90..5b21444 100644 --- a/place_tool/op.py +++ b/place_tool/op.py @@ -556,24 +556,24 @@ def handle_multi_obj(self, context, event): world_loc = location - with store_objs_mx([self.tmp_parent], self.stop_moving(exclude_obj_list=[self.tg_obj])): - z_offset = Vector((0, 0, self.z_offset)) - self.tmp_parent.location = world_loc - context.view_layer.update() - if place_tool_props().orient == 'NORMAL': + with store_objs_mx([self.tmp_parent], self.stop_moving(exclude_obj_list=[self.tg_obj])): + z_offset = Vector((0, 0, self.z_offset)) + self.tmp_parent.location = world_loc + context.view_layer.update() + if place_tool_props().orient == 'NORMAL': + context.view_layer.update() + self.clear_rotate(context, self.tmp_parent) + self.tmp_parent.matrix_world = self.tmp_parent.matrix_world @ Matrix.Translation(z_offset) + if hasattr(self, 'objs_A') and context.object in self.selected_objs: + self.objs_A.bvh_tree_update() + + offset_mx = Matrix.Translation(world_loc - self.center) + ALIGN_OBJS['bbox_pts'] = self.objs_A.get_bbox_pts() + ALIGN_OBJS['top'] = offset_mx @ self.top + ALIGN_OBJS['center'] = offset_mx @ self.top # 使用默认center容易闪烁,故改用top + ALIGN_OBJS['bottom'] = self.tmp_parent.location + ALIGN_OBJS['size'] = self.objs_A.size context.view_layer.update() - self.clear_rotate(context, self.tmp_parent) - self.tmp_parent.matrix_world = self.tmp_parent.matrix_world @ Matrix.Translation(z_offset) - if hasattr(self, 'objs_A') and context.object in self.selected_objs: - self.objs_A.bvh_tree_update() - - offset_mx = Matrix.Translation(world_loc - self.center) - ALIGN_OBJS['bbox_pts'] = self.objs_A.get_bbox_pts() - ALIGN_OBJS['top'] = offset_mx @ self.top - ALIGN_OBJS['center'] = offset_mx @ self.top # 使用默认center容易闪烁,故改用top - ALIGN_OBJS['bottom'] = self.tmp_parent.location - ALIGN_OBJS['size'] = self.objs_A.size - context.view_layer.update() def clear_rotate(self, context, obj): """清除除了local z以外轴向的旋转"""