Last active
August 2, 2025 10:36
-
-
Save soswow/0680771e4c015101bcf66b813ef53e8d to your computer and use it in GitHub Desktop.
Blender persistent xray feature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| def debug(msg): | |
| print(f"[DEBUG] {msg}") | |
| # Register custom properties | |
| def register_properties(): | |
| wm = bpy.types.WindowManager | |
| debug("Registering properties") | |
| wm.xray_sync_enabled = bpy.props.BoolProperty( | |
| name="Sync X-Ray", | |
| description="Persist X-Ray state across mode changes", | |
| default=False | |
| ) | |
| wm.xray_synced_state = bpy.props.BoolProperty( | |
| name="Last X-Ray State", | |
| description="Stores last X-Ray state", | |
| default=False | |
| ) | |
| wm.xray_last_mode = bpy.props.StringProperty( | |
| name="Last Mode", | |
| default="" | |
| ) | |
| wm.xray_current_state = bpy.props.BoolProperty( | |
| name="Current X-Ray State", | |
| description="Tracks current X-Ray state for change detection", | |
| default=False | |
| ) | |
| def unregister_properties(): | |
| debug("Unregistering properties") | |
| wm = bpy.types.WindowManager | |
| for prop in ["xray_sync_enabled", "xray_synced_state", "xray_last_mode", "xray_current_state"]: | |
| if hasattr(wm, prop): | |
| debug(f"Removing property: {prop}") | |
| delattr(wm, prop) | |
| # Simple timer to check xray state changes | |
| def check_xray_changes(): | |
| try: | |
| wm = bpy.context.window_manager | |
| if not wm.xray_sync_enabled: | |
| return 0.5 # Check every 0.5 seconds when sync is disabled | |
| # Get current xray state | |
| current_state = False | |
| for area in bpy.context.screen.areas: | |
| if area.type == 'VIEW_3D': | |
| for space in area.spaces: | |
| if space.type == 'VIEW_3D': | |
| current_state = space.shading.show_xray | |
| break | |
| if current_state is not None: | |
| break | |
| # Check if state changed | |
| if wm.xray_current_state != current_state: | |
| debug(f"X-Ray state changed from {wm.xray_current_state} to {current_state}") | |
| wm.xray_current_state = current_state | |
| wm.xray_synced_state = current_state | |
| debug(f"Updated synced_state to {current_state}") | |
| return 0.5 # Check every 0.5 seconds | |
| except Exception as e: | |
| print(f"Error in check_xray_changes: {e}") | |
| return 0.5 | |
| # Handler to restore X-Ray state after mode change | |
| def sync_xray_on_mode_change(scene): | |
| try: | |
| debug("sync_xray_on_mode_change called") | |
| wm = bpy.context.window_manager | |
| current_mode = bpy.context.mode | |
| debug(f"Current mode: {current_mode}, Last mode: {wm.xray_last_mode}") | |
| if not wm.xray_sync_enabled: | |
| debug("Sync not enabled, updating last mode") | |
| wm.xray_last_mode = current_mode | |
| return | |
| if wm.xray_last_mode != current_mode: | |
| debug("Mode changed. Restoring X-Ray state") | |
| wm.xray_last_mode = current_mode | |
| for area in bpy.context.screen.areas: | |
| if area.type == 'VIEW_3D': | |
| for space in area.spaces: | |
| if space.type == 'VIEW_3D': | |
| debug(f"Setting show_xray = {wm.xray_synced_state}") | |
| space.shading.show_xray = wm.xray_synced_state | |
| except Exception as e: | |
| print(f"Error in sync_xray_on_mode_change: {e}") | |
| # Handler to track user toggling X-Ray directly | |
| def track_manual_xray_toggle(scene): | |
| try: | |
| debug("track_manual_xray_toggle called") | |
| wm = bpy.context.window_manager | |
| # Always track changes, regardless of sync state | |
| found_viewport = False | |
| for area in bpy.context.screen.areas: | |
| if area.type == 'VIEW_3D': | |
| for space in area.spaces: | |
| if space.type == 'VIEW_3D': | |
| found_viewport = True | |
| current_state = space.shading.show_xray | |
| debug(f"Found viewport, current show_xray: {current_state}") | |
| if wm.xray_synced_state != current_state: | |
| debug(f"X-Ray state changed from {wm.xray_synced_state} to {current_state}") | |
| # Only update synced state if sync is enabled | |
| if wm.xray_sync_enabled: | |
| wm.xray_synced_state = current_state | |
| debug(f"Updated synced_state to {current_state}") | |
| else: | |
| debug("Sync not enabled, but tracking change") | |
| else: | |
| debug(f"No change in X-Ray state: {current_state}") | |
| break | |
| if found_viewport: | |
| break | |
| if not found_viewport: | |
| debug("No 3D viewport found") | |
| except Exception as e: | |
| print(f"Error in track_manual_xray_toggle: {e}") | |
| # Operator for toolbar button | |
| class VIEW3D_OT_toggle_xray_sync(bpy.types.Operator): | |
| bl_idname = "view3d.toggle_xray_sync" | |
| bl_label = "Toggle X-Ray Sync" | |
| bl_description = "Toggle persistent X-Ray mode behavior" | |
| def execute(self, context): | |
| try: | |
| wm = context.window_manager | |
| wm.xray_sync_enabled = not wm.xray_sync_enabled | |
| debug(f"Toggled xray_sync_enabled to {wm.xray_sync_enabled}") | |
| if wm.xray_sync_enabled: | |
| for area in context.screen.areas: | |
| if area.type == 'VIEW_3D': | |
| for space in area.spaces: | |
| if space.type == 'VIEW_3D': | |
| wm.xray_synced_state = space.shading.show_xray | |
| wm.xray_current_state = space.shading.show_xray | |
| debug(f"Saved current show_xray state: {wm.xray_synced_state}") | |
| debug(f"Initialized current_state: {wm.xray_current_state}") | |
| wm.xray_last_mode = context.mode | |
| debug(f"Set last_mode to {wm.xray_last_mode}") | |
| return {'FINISHED'} | |
| except Exception as e: | |
| self.report({'ERROR'}, f"Error toggling X-Ray sync: {e}") | |
| return {'CANCELLED'} | |
| # Draw toggle in Viewport header | |
| def draw_xray_sync_button(self, context): | |
| wm = context.window_manager | |
| layout = self.layout | |
| # Safety check - only draw if properties are registered | |
| if not hasattr(wm, 'xray_sync_enabled'): | |
| return | |
| # Main sync button | |
| layout.operator( | |
| "view3d.toggle_xray_sync", | |
| text="", | |
| icon="XRAY", | |
| depress=wm.xray_sync_enabled | |
| ) | |
| # Register/unregister | |
| classes = (VIEW3D_OT_toggle_xray_sync,) | |
| def register(): | |
| debug("Registering addon") | |
| for cls in classes: | |
| bpy.utils.register_class(cls) | |
| register_properties() | |
| bpy.types.VIEW3D_HT_header.append(draw_xray_sync_button) | |
| if sync_xray_on_mode_change not in bpy.app.handlers.depsgraph_update_post: | |
| bpy.app.handlers.depsgraph_update_post.append(sync_xray_on_mode_change) | |
| debug("Added sync_xray_on_mode_change handler") | |
| # Add timer to check for xray changes | |
| bpy.app.timers.register(check_xray_changes) | |
| debug("Added check_xray_changes timer") | |
| def unregister(): | |
| debug("Unregistering addon") | |
| # Remove handlers safely | |
| try: | |
| # Remove sync handler by function name | |
| for handler in bpy.app.handlers.depsgraph_update_post: | |
| if handler.__name__ == 'sync_xray_on_mode_change': | |
| bpy.app.handlers.depsgraph_update_post.remove(handler) | |
| debug("Removed sync_xray_on_mode_change handler") | |
| break | |
| except Exception as e: | |
| print(f"Error removing sync handler: {e}") | |
| # Remove timer | |
| try: | |
| # Find and remove timer by function name | |
| for timer in bpy.app.timers.get_list(): | |
| if timer.__name__ == 'check_xray_changes': | |
| bpy.app.timers.unregister(timer) | |
| debug("Removed check_xray_changes timer") | |
| break | |
| except Exception as e: | |
| print(f"Error removing timer: {e}") | |
| # Remove UI elements safely | |
| try: | |
| # Remove by function name instead of direct reference | |
| for draw_func in bpy.types.VIEW3D_HT_header.draw_funcs: | |
| if draw_func.__name__ == 'draw_xray_sync_button': | |
| bpy.types.VIEW3D_HT_header.remove(draw_func) | |
| debug("Removed draw_xray_sync_button from header") | |
| break | |
| except Exception as e: | |
| print(f"Error removing header button: {e}") | |
| # Unregister properties and classes | |
| unregister_properties() | |
| # remove_xray_property_update() # This line is removed as per the edit hint | |
| for cls in reversed(classes): | |
| try: | |
| bpy.utils.unregister_class(cls) | |
| except Exception as e: | |
| print(f"Error unregistering class {cls.__name__}: {e}") | |
| if __name__ == "__main__": | |
| unregister() | |
| register() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| from bpy import context as C, data as D | |
| from bpy.types import SpaceView3D | |
| def detect_xray_change(): | |
| for area in C.screen.areas: | |
| if area.type == 'VIEW_3D': | |
| for space in area.spaces: | |
| if space.type == 'VIEW_3D': | |
| if bpy.types.SpaceView3D.current_xray_state != space.shading.show_xray: | |
| bpy.types.SpaceView3D.current_xray_state = space.shading.show_xray | |
| print(bpy.types.SpaceView3D.current_xray_state) | |
| def register(): | |
| if not hasattr(bpy.types.SpaceView3D, 'my_handlers'): | |
| print("my_handlers not found, creating it") | |
| bpy.types.SpaceView3D.my_handlers = [] | |
| if not hasattr(bpy.types.SpaceView3D, 'current_xray_state'): | |
| print("current_xray_state not found, creating it") | |
| bpy.types.SpaceView3D.current_xray_state = None | |
| handler = SpaceView3D.draw_handler_add(detect_xray_change, (), 'WINDOW', 'PRE_VIEW') | |
| SpaceView3D.my_handlers.append(handler) | |
| def unregister(): | |
| for handler in SpaceView3D.my_handlers: | |
| SpaceView3D.draw_handler_remove(handler, 'WINDOW') | |
| bpy.types.SpaceView3D.my_handlers = [] | |
| if hasattr(bpy.types.SpaceView3D, 'current_xray_state'): | |
| print("current_xray_state found, deleting it") | |
| del bpy.types.SpaceView3D.current_xray_state | |
| if __name__ == "__main__": | |
| unregister() | |
| register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment