1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19bl_info = { 20 "name": "Stored Views", 21 "description": "Save and restore User defined views, pov, layers and display configs", 22 "author": "nfloyd, Francesco Siddi", 23 "version": (0, 3, 7), 24 "blender": (2, 80, 0), 25 "location": "View3D > Properties > Stored Views", 26 "warning": "", 27 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.5/" 28 "Py/Scripts/3D_interaction/stored_views", 29 "category": "3D View" 30} 31 32""" 33ACKNOWLEDGMENT 34============== 35import/export functionality is mostly based 36on Bart Crouch's Theme Manager Addon 37 38TODO: quadview complete support : investigate. Where's the data? 39TODO: lock_camera_and_layers. investigate usage 40TODO: list reordering 41 42NOTE: logging setup has to be provided by the user in a separate config file 43 as Blender will not try to configure logging by default in an add-on 44 The Config File should be in the Blender Config folder > /scripts/startup/config_logging.py 45 For setting up /location of the config folder see: 46 https://docs.blender.org/manual/en/latest/getting_started/ 47 installing/configuration/directories.html 48 For configuring logging itself in the file, general Python documentation should work 49 As the logging calls are not configured, they can be kept in the other modules of this add-on 50 and will not have output until the logging configuration is set up 51""" 52 53 54import bpy 55from bpy.props import ( 56 BoolProperty, 57 IntProperty, 58 PointerProperty, 59) 60from bpy.types import ( 61 AddonPreferences, 62 Operator, 63 Panel 64) 65 66import logging 67module_logger = logging.getLogger(__name__) 68 69import gzip 70import os 71import pickle 72import shutil 73 74from bpy_extras.io_utils import ( 75 ExportHelper, 76 ImportHelper, 77) 78 79import blf 80 81import hashlib 82import bpy 83 84 85# Utility function get preferences setting for exporters 86def get_preferences(): 87 # replace the key if the add-on name changes 88 addon = bpy.context.preferences.addons[__package__] 89 show_warn = (addon.preferences.show_exporters if addon else False) 90 91 return show_warn 92 93 94class StoredView(): 95 def __init__(self, mode, index=None): 96 self.logger = logging.getLogger('%s.StoredView' % __name__) 97 self.scene = bpy.context.scene 98 self.view3d = bpy.context.space_data 99 self.index = index 100 self.data_store = DataStore(mode=mode) 101 102 def save(self): 103 if self.index == -1: 104 stored_view, self.index = self.data_store.create() 105 else: 106 stored_view = self.data_store.get(self.index) 107 self.from_v3d(stored_view) 108 self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name)) 109 110 def set(self): 111 stored_view = self.data_store.get(self.index) 112 self.update_v3d(stored_view) 113 self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name)) 114 115 def from_v3d(self, stored_view): 116 raise NotImplementedError("Subclass must implement abstract method") 117 118 def update_v3d(self, stored_view): 119 raise NotImplementedError("Subclass must implement abstract method") 120 121 @staticmethod 122 def is_modified(context, stored_view): 123 raise NotImplementedError("Subclass must implement abstract method") 124 125 126class POV(StoredView): 127 def __init__(self, index=None): 128 super().__init__(mode='POV', index=index) 129 self.logger = logging.getLogger('%s.POV' % __name__) 130 131 def from_v3d(self, stored_view): 132 view3d = self.view3d 133 region3d = view3d.region_3d 134 135 stored_view.distance = region3d.view_distance 136 stored_view.location = region3d.view_location 137 stored_view.rotation = region3d.view_rotation 138 stored_view.perspective_matrix_md5 = POV._get_perspective_matrix_md5(region3d) 139 stored_view.perspective = region3d.view_perspective 140 stored_view.lens = view3d.lens 141 stored_view.clip_start = view3d.clip_start 142 stored_view.clip_end = view3d.clip_end 143 144 if region3d.view_perspective == 'CAMERA': 145 stored_view.camera_type = view3d.camera.type # type : 'CAMERA' or 'MESH' 146 stored_view.camera_name = view3d.camera.name # store string instead of object 147 if view3d.lock_object is not None: 148 stored_view.lock_object_name = view3d.lock_object.name # idem 149 else: 150 stored_view.lock_object_name = "" 151 stored_view.lock_cursor = view3d.lock_cursor 152 stored_view.cursor_location = view3d.cursor_location 153 154 def update_v3d(self, stored_view): 155 view3d = self.view3d 156 region3d = view3d.region_3d 157 region3d.view_distance = stored_view.distance 158 region3d.view_location = stored_view.location 159 region3d.view_rotation = stored_view.rotation 160 region3d.view_perspective = stored_view.perspective 161 view3d.lens = stored_view.lens 162 view3d.clip_start = stored_view.clip_start 163 view3d.clip_end = stored_view.clip_end 164 view3d.lock_cursor = stored_view.lock_cursor 165 if stored_view.lock_cursor is True: 166 # update cursor only if view is locked to cursor 167 view3d.cursor_location = stored_view.cursor_location 168 169 if stored_view.perspective == "CAMERA": 170 171 lock_obj = self._get_object(stored_view.lock_object_name) 172 if lock_obj: 173 view3d.lock_object = lock_obj 174 else: 175 cam = self._get_object(stored_view.camera_name) 176 if cam: 177 view3d.camera = cam 178 179 @staticmethod 180 def _get_object(name, pointer=None): 181 return bpy.data.objects.get(name) 182 183 @staticmethod 184 def is_modified(context, stored_view): 185 # TODO: check for others param, currently only perspective 186 # and perspective_matrix are checked 187 POV.logger = logging.getLogger('%s.POV' % __name__) 188 view3d = context.space_data 189 region3d = view3d.region_3d 190 if region3d.view_perspective != stored_view.perspective: 191 POV.logger.debug('view_perspective') 192 return True 193 194 md5 = POV._get_perspective_matrix_md5(region3d) 195 if (md5 != stored_view.perspective_matrix_md5 and 196 region3d.view_perspective != "CAMERA"): 197 POV.logger.debug('perspective_matrix') 198 return True 199 200 return False 201 202 @staticmethod 203 def _get_perspective_matrix_md5(region3d): 204 md5 = hashlib.md5(str(region3d.perspective_matrix).encode('utf-8')).hexdigest() 205 return md5 206 207 208class Layers(StoredView): 209 def __init__(self, index=None): 210 super().__init__(mode='LAYERS', index=index) 211 self.logger = logging.getLogger('%s.Layers' % __name__) 212 213 def from_v3d(self, stored_view): 214 view3d = self.view3d 215 stored_view.view_layers = view3d.layers 216 stored_view.scene_layers = self.scene.layers 217 stored_view.lock_camera_and_layers = view3d.lock_camera_and_layers 218 219 def update_v3d(self, stored_view): 220 view3d = self.view3d 221 view3d.lock_camera_and_layers = stored_view.lock_camera_and_layers 222 if stored_view.lock_camera_and_layers is True: 223 self.scene.layers = stored_view.scene_layers 224 else: 225 view3d.layers = stored_view.view_layers 226 227 @staticmethod 228 def is_modified(context, stored_view): 229 Layers.logger = logging.getLogger('%s.Layers' % __name__) 230 if stored_view.lock_camera_and_layers != context.space_data.lock_camera_and_layers: 231 Layers.logger.debug('lock_camera_and_layers') 232 return True 233 if stored_view.lock_camera_and_layers is True: 234 for i in range(20): 235 if stored_view.scene_layers[i] != context.scene.layers[i]: 236 Layers.logger.debug('scene_layers[%s]' % (i, )) 237 return True 238 else: 239 for i in range(20): 240 if stored_view.view_layers[i] != context.space_data.view3d.layers[i]: 241 return True 242 return False 243 244 245class Display(StoredView): 246 def __init__(self, index=None): 247 super().__init__(mode='DISPLAY', index=index) 248 self.logger = logging.getLogger('%s.Display' % __name__) 249 250 def from_v3d(self, stored_view): 251 view3d = self.view3d 252 stored_view.viewport_shade = view3d.viewport_shade 253 stored_view.show_only_render = view3d.show_only_render 254 stored_view.show_outline_selected = view3d.show_outline_selected 255 stored_view.show_all_objects_origin = view3d.show_all_objects_origin 256 stored_view.show_relationship_lines = view3d.show_relationship_lines 257 stored_view.show_floor = view3d.show_floor 258 stored_view.show_axis_x = view3d.show_axis_x 259 stored_view.show_axis_y = view3d.show_axis_y 260 stored_view.show_axis_z = view3d.show_axis_z 261 stored_view.grid_lines = view3d.grid_lines 262 stored_view.grid_scale = view3d.grid_scale 263 stored_view.grid_subdivisions = view3d.grid_subdivisions 264 stored_view.material_mode = self.scene.game_settings.material_mode 265 stored_view.show_textured_solid = view3d.show_textured_solid 266 267 def update_v3d(self, stored_view): 268 view3d = self.view3d 269 view3d.viewport_shade = stored_view.viewport_shade 270 view3d.show_only_render = stored_view.show_only_render 271 view3d.show_outline_selected = stored_view.show_outline_selected 272 view3d.show_all_objects_origin = stored_view.show_all_objects_origin 273 view3d.show_relationship_lines = stored_view.show_relationship_lines 274 view3d.show_floor = stored_view.show_floor 275 view3d.show_axis_x = stored_view.show_axis_x 276 view3d.show_axis_y = stored_view.show_axis_y 277 view3d.show_axis_z = stored_view.show_axis_z 278 view3d.grid_lines = stored_view.grid_lines 279 view3d.grid_scale = stored_view.grid_scale 280 view3d.grid_subdivisions = stored_view.grid_subdivisions 281 self.scene.game_settings.material_mode = stored_view.material_mode 282 view3d.show_textured_solid = stored_view.show_textured_solid 283 284 @staticmethod 285 def is_modified(context, stored_view): 286 Display.logger = logging.getLogger('%s.Display' % __name__) 287 view3d = context.space_data 288 excludes = ["material_mode", "quad_view", "lock_rotation", "show_sync_view", "use_box_clip", "name"] 289 for k, v in stored_view.items(): 290 if k not in excludes: 291 if getattr(view3d, k) != getattr(stored_view, k): 292 return True 293 294 if stored_view.material_mode != context.scene.game_settings.material_mode: 295 Display.logger.debug('material_mode') 296 return True 297 298 299class View(StoredView): 300 def __init__(self, index=None): 301 super().__init__(mode='VIEW', index=index) 302 self.logger = logging.getLogger('%s.View' % __name__) 303 self.pov = POV() 304 self.layers = Layers() 305 self.display = Display() 306 307 def from_v3d(self, stored_view): 308 self.pov.from_v3d(stored_view.pov) 309 self.layers.from_v3d(stored_view.layers) 310 self.display.from_v3d(stored_view.display) 311 312 def update_v3d(self, stored_view): 313 self.pov.update_v3d(stored_view.pov) 314 self.layers.update_v3d(stored_view.layers) 315 self.display.update_v3d(stored_view.display) 316 317 @staticmethod 318 def is_modified(context, stored_view): 319 if POV.is_modified(context, stored_view.pov) or \ 320 Layers.is_modified(context, stored_view.layers) or \ 321 Display.is_modified(context, stored_view.display): 322 return True 323 return False 324 325 326class DataStore(): 327 def __init__(self, scene=None, mode=None): 328 if scene is None: 329 scene = bpy.context.scene 330 stored_views = scene.stored_views 331 self.mode = mode 332 333 if mode is None: 334 self.mode = stored_views.mode 335 336 if self.mode == 'VIEW': 337 self.list = stored_views.view_list 338 self.current_index = stored_views.current_indices[0] 339 elif self.mode == 'POV': 340 self.list = stored_views.pov_list 341 self.current_index = stored_views.current_indices[1] 342 elif self.mode == 'LAYERS': 343 self.list = stored_views.layers_list 344 self.current_index = stored_views.current_indices[2] 345 elif self.mode == 'DISPLAY': 346 self.list = stored_views.display_list 347 self.current_index = stored_views.current_indices[3] 348 349 def create(self): 350 item = self.list.add() 351 item.name = self._generate_name() 352 index = len(self.list) - 1 353 self._set_current_index(index) 354 return item, index 355 356 def get(self, index): 357 self._set_current_index(index) 358 return self.list[index] 359 360 def delete(self, index): 361 if self.current_index > index: 362 self._set_current_index(self.current_index - 1) 363 elif self.current_index == index: 364 self._set_current_index(-1) 365 366 self.list.remove(index) 367 368 def _set_current_index(self, index): 369 self.current_index = index 370 mode = self.mode 371 stored_views = bpy.context.scene.stored_views 372 if mode == 'VIEW': 373 stored_views.current_indices[0] = index 374 elif mode == 'POV': 375 stored_views.current_indices[1] = index 376 elif mode == 'LAYERS': 377 stored_views.current_indices[2] = index 378 elif mode == 'DISPLAY': 379 stored_views.current_indices[3] = index 380 381 def _generate_name(self): 382 default_name = str(self.mode) 383 names = [] 384 for i in self.list: 385 i_name = i.name 386 if i_name.startswith(default_name): 387 names.append(i_name) 388 names.sort() 389 try: 390 l_name = names[-1] 391 post_fix = l_name.rpartition('.')[2] 392 if post_fix.isnumeric(): 393 post_fix = str(int(post_fix) + 1).zfill(3) 394 else: 395 if post_fix == default_name: 396 post_fix = "001" 397 return default_name + "." + post_fix 398 except: 399 return default_name 400 401 @staticmethod 402 def sanitize_data(scene): 403 404 def check_objects_references(mode, list): 405 to_remove = [] 406 for i, list_item in enumerate(list.items()): 407 key, item = list_item 408 if mode == 'POV' or mode == 'VIEWS': 409 if mode == 'VIEWS': 410 item = item.pov 411 412 if item.perspective == "CAMERA": 413 414 camera = bpy.data.objects.get(item.camera_name) 415 if camera is None: 416 try: # pick a default camera TODO: ask to pick? 417 camera = bpy.data.cameras[0] 418 item.camera_name = camera.name 419 except: # couldn't find a camera in the scene 420 pass 421 422 obj = bpy.data.objects.get(item.lock_object_name) 423 if obj is None and camera is None: 424 to_remove.append(i) 425 426 for i in reversed(to_remove): 427 list.remove(i) 428 429 modes = ['POV', 'VIEW', 'DISPLAY', 'LAYERS'] 430 for mode in modes: 431 data = DataStore(scene=scene, mode=mode) 432 check_objects_references(mode, data.list) 433 434 435def stored_view_factory(mode, *args, **kwargs): 436 if mode == 'POV': 437 return POV(*args, **kwargs) 438 elif mode == 'LAYERS': 439 return Layers(*args, **kwargs) 440 elif mode == 'DISPLAY': 441 return Display(*args, **kwargs) 442 elif mode == 'VIEW': 443 return View(*args, **kwargs) 444 445""" 446 If view name display is enabled, 447 it will check periodically if the view has been modified 448 since last set. 449 get_preferences_timer() is the time in seconds between these checks. 450 It can be increased, if the view become sluggish 451 It is set in the add-on preferences 452""" 453 454 455# Utility function get_preferences_timer for update of 3d view draw 456def get_preferences_timer(): 457 # replace the key if the add-on name changes 458 # TODO: expose refresh rate to ui??? 459 addon = bpy.context.preferences.addons[__package__] 460 timer_update = (addon.preferences.view_3d_update_rate if addon else False) 461 462 return timer_update 463 464 465def init_draw(context=None): 466 if context is None: 467 context = bpy.context 468 469 if "stored_views_osd" not in context.window_manager: 470 context.window_manager["stored_views_osd"] = False 471 472 if not context.window_manager["stored_views_osd"]: 473 context.window_manager["stored_views_osd"] = True 474 bpy.ops.stored_views.draw() 475 476 477def _draw_callback_px(self, context): 478 if context.area and context.area.type == 'VIEW_3D': 479 r_width = text_location = context.region.width 480 r_height = context.region.height 481 font_id = 0 # TODO: need to find out how best to get font_id 482 483 blf.size(font_id, 11, context.preferences.system.dpi) 484 text_size = blf.dimensions(0, self.view_name) 485 486 # compute the text location 487 text_location = 0 488 overlap = context.preferences.system.use_region_overlap 489 if overlap: 490 for region in context.area.regions: 491 if region.type == "UI": 492 text_location = r_width - region.width 493 494 text_x = text_location - text_size[0] - 10 495 text_y = r_height - text_size[1] - 8 496 blf.position(font_id, text_x, text_y, 0) 497 blf.draw(font_id, self.view_name) 498 499 500class VIEW3D_OT_stored_views_draw(Operator): 501 bl_idname = "stored_views.draw" 502 bl_label = "Show current" 503 bl_description = "Toggle the display current view name in the view 3D" 504 505 _handle = None 506 _timer = None 507 508 @staticmethod 509 def handle_add(self, context): 510 VIEW3D_OT_stored_views_draw._handle = bpy.types.SpaceView3D.draw_handler_add( 511 _draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') 512 VIEW3D_OT_stored_views_draw._timer = \ 513 context.window_manager.event_timer_add(get_preferences_timer(), context.window) 514 515 @staticmethod 516 def handle_remove(context): 517 if VIEW3D_OT_stored_views_draw._handle is not None: 518 bpy.types.SpaceView3D.draw_handler_remove(VIEW3D_OT_stored_views_draw._handle, 'WINDOW') 519 if VIEW3D_OT_stored_views_draw._timer is not None: 520 context.window_manager.event_timer_remove(VIEW3D_OT_stored_views_draw._timer) 521 VIEW3D_OT_stored_views_draw._handle = None 522 VIEW3D_OT_stored_views_draw._timer = None 523 524 @classmethod 525 def poll(cls, context): 526 # return context.mode == 'OBJECT' 527 return True 528 529 def modal(self, context, event): 530 if context.area: 531 context.area.tag_redraw() 532 533 if not context.area or context.area.type != "VIEW_3D": 534 return {"PASS_THROUGH"} 535 536 data = DataStore() 537 stored_views = context.scene.stored_views 538 539 if len(data.list) > 0 and \ 540 data.current_index >= 0 and \ 541 not stored_views.view_modified: 542 543 if not stored_views.view_modified: 544 sv = data.list[data.current_index] 545 self.view_name = sv.name 546 if event.type == 'TIMER': 547 is_modified = False 548 if data.mode == 'VIEW': 549 is_modified = View.is_modified(context, sv) 550 elif data.mode == 'POV': 551 is_modified = POV.is_modified(context, sv) 552 elif data.mode == 'LAYERS': 553 is_modified = Layers.is_modified(context, sv) 554 elif data.mode == 'DISPLAY': 555 is_modified = Display.is_modified(context, sv) 556 if is_modified: 557 module_logger.debug( 558 'view modified - index: %s name: %s' % (data.current_index, sv.name) 559 ) 560 self.view_name = "" 561 stored_views.view_modified = is_modified 562 563 return {"PASS_THROUGH"} 564 else: 565 module_logger.debug('exit') 566 context.window_manager["stored_views_osd"] = False 567 VIEW3D_OT_stored_views_draw.handle_remove(context) 568 569 return {'FINISHED'} 570 571 def execute(self, context): 572 if context.area.type == "VIEW_3D": 573 self.view_name = "" 574 VIEW3D_OT_stored_views_draw.handle_add(self, context) 575 context.window_manager.modal_handler_add(self) 576 577 return {"RUNNING_MODAL"} 578 else: 579 self.report({"WARNING"}, "View3D not found. Operation Cancelled") 580 581 return {"CANCELLED"} 582 583class VIEW3D_OT_stored_views_initialize(Operator): 584 bl_idname = "view3d.stored_views_initialize" 585 bl_label = "Initialize" 586 587 @classmethod 588 def poll(cls, context): 589 return not hasattr(bpy.types.Scene, 'stored_views') 590 591 def execute(self, context): 592 bpy.types.Scene.stored_views: PointerProperty( 593 type=properties.StoredViewsData 594 ) 595 scenes = bpy.data.scenes 596 data = DataStore() 597 for scene in scenes: 598 DataStore.sanitize_data(scene) 599 return {'FINISHED'} 600 601 602from bpy.types import PropertyGroup 603from bpy.props import ( 604 BoolProperty, 605 BoolVectorProperty, 606 CollectionProperty, 607 FloatProperty, 608 FloatVectorProperty, 609 EnumProperty, 610 IntProperty, 611 IntVectorProperty, 612 PointerProperty, 613 StringProperty, 614 ) 615 616 617class POVData(PropertyGroup): 618 distance: FloatProperty() 619 location: FloatVectorProperty( 620 subtype='TRANSLATION' 621 ) 622 rotation: FloatVectorProperty( 623 subtype='QUATERNION', 624 size=4 625 ) 626 name: StringProperty() 627 perspective: EnumProperty( 628 items=[('PERSP', '', ''), 629 ('ORTHO', '', ''), 630 ('CAMERA', '', '')] 631 ) 632 lens: FloatProperty() 633 clip_start: FloatProperty() 634 clip_end: FloatProperty() 635 lock_cursor: BoolProperty() 636 cursor_location: FloatVectorProperty() 637 perspective_matrix_md5: StringProperty() 638 camera_name: StringProperty() 639 camera_type: StringProperty() 640 lock_object_name: StringProperty() 641 642 643class LayersData(PropertyGroup): 644 view_layers: BoolVectorProperty(size=20) 645 scene_layers: BoolVectorProperty(size=20) 646 lock_camera_and_layers: BoolProperty() 647 name: StringProperty() 648 649 650class DisplayData(PropertyGroup): 651 name: StringProperty() 652 viewport_shade: EnumProperty( 653 items=[('BOUNDBOX', 'BOUNDBOX', 'BOUNDBOX'), 654 ('WIREFRAME', 'WIREFRAME', 'WIREFRAME'), 655 ('SOLID', 'SOLID', 'SOLID'), 656 ('TEXTURED', 'TEXTURED', 'TEXTURED'), 657 ('MATERIAL', 'MATERIAL', 'MATERIAL'), 658 ('RENDERED', 'RENDERED', 'RENDERED')] 659 ) 660 show_only_render: BoolProperty() 661 show_outline_selected: BoolProperty() 662 show_all_objects_origin: BoolProperty() 663 show_relationship_lines: BoolProperty() 664 show_floor: BoolProperty() 665 show_axis_x: BoolProperty() 666 show_axis_y: BoolProperty() 667 show_axis_z: BoolProperty() 668 grid_lines: IntProperty() 669 grid_scale: FloatProperty() 670 grid_subdivisions: IntProperty() 671 material_mode: StringProperty() 672 show_textured_solid: BoolProperty() 673 quad_view: BoolProperty() 674 lock_rotation: BoolProperty() 675 show_sync_view: BoolProperty() 676 use_box_clip: BoolProperty() 677 678 679class ViewData(PropertyGroup): 680 pov: PointerProperty( 681 type=POVData 682 ) 683 layers: PointerProperty( 684 type=LayersData 685 ) 686 display: PointerProperty( 687 type=DisplayData 688 ) 689 name: StringProperty() 690 691 692class StoredViewsData(PropertyGroup): 693 pov_list: CollectionProperty( 694 type=POVData 695 ) 696 layers_list: CollectionProperty( 697 type=LayersData 698 ) 699 display_list: CollectionProperty( 700 type=DisplayData 701 ) 702 view_list: CollectionProperty( 703 type=ViewData 704 ) 705 mode: EnumProperty( 706 name="Mode", 707 items=[('VIEW', "View", "3D View settings"), 708 ('POV', "POV", "POV settings"), 709 ('LAYERS', "Layers", "Layers settings"), 710 ('DISPLAY', "Display", "Display settings")], 711 default='VIEW' 712 ) 713 current_indices: IntVectorProperty( 714 size=4, 715 default=[-1, -1, -1, -1] 716 ) 717 view_modified: BoolProperty( 718 default=False 719 ) 720 721class VIEW3D_OT_stored_views_save(Operator): 722 bl_idname = "stored_views.save" 723 bl_label = "Save Current" 724 bl_description = "Save the view 3d current state" 725 726 index: IntProperty() 727 728 def execute(self, context): 729 mode = context.scene.stored_views.mode 730 sv = stored_view_factory(mode, self.index) 731 sv.save() 732 context.scene.stored_views.view_modified = False 733 init_draw(context) 734 735 return {'FINISHED'} 736 737 738class VIEW3D_OT_stored_views_set(Operator): 739 bl_idname = "stored_views.set" 740 bl_label = "Set" 741 bl_description = "Update the view 3D according to this view" 742 743 index: IntProperty() 744 745 def execute(self, context): 746 mode = context.scene.stored_views.mode 747 sv = stored_view_factory(mode, self.index) 748 sv.set() 749 context.scene.stored_views.view_modified = False 750 init_draw(context) 751 752 return {'FINISHED'} 753 754 755class VIEW3D_OT_stored_views_delete(Operator): 756 bl_idname = "stored_views.delete" 757 bl_label = "Delete" 758 bl_description = "Delete this view" 759 760 index: IntProperty() 761 762 def execute(self, context): 763 data = DataStore() 764 data.delete(self.index) 765 766 return {'FINISHED'} 767 768 769class VIEW3D_OT_New_Camera_to_View(Operator): 770 bl_idname = "stored_views.newcamera" 771 bl_label = "New Camera To View" 772 bl_description = "Add a new Active Camera and align it to this view" 773 774 @classmethod 775 def poll(cls, context): 776 return ( 777 context.space_data is not None and 778 context.space_data.type == 'VIEW_3D' and 779 context.space_data.region_3d.view_perspective != 'CAMERA' 780 ) 781 782 def execute(self, context): 783 784 if bpy.ops.object.mode_set.poll(): 785 bpy.ops.object.mode_set(mode='OBJECT') 786 787 bpy.ops.object.camera_add() 788 cam = context.active_object 789 cam.name = "View_Camera" 790 # make active camera by hand 791 context.scene.camera = cam 792 793 bpy.ops.view3d.camera_to_view() 794 return {'FINISHED'} 795 796 797# Camera marker & switcher by Fsiddi 798class VIEW3D_OT_SetSceneCamera(Operator): 799 bl_idname = "cameraselector.set_scene_camera" 800 bl_label = "Set Scene Camera" 801 bl_description = "Set chosen camera as the scene's active camera" 802 803 hide_others = False 804 805 def execute(self, context): 806 chosen_camera = context.active_object 807 scene = context.scene 808 809 if self.hide_others: 810 for c in [o for o in scene.objects if o.type == 'CAMERA']: 811 c.hide = (c != chosen_camera) 812 scene.camera = chosen_camera 813 bpy.ops.object.select_all(action='DESELECT') 814 chosen_camera.select_set(True) 815 return {'FINISHED'} 816 817 def invoke(self, context, event): 818 if event.ctrl: 819 self.hide_others = True 820 821 return self.execute(context) 822 823 824class VIEW3D_OT_PreviewSceneCamera(Operator): 825 bl_idname = "cameraselector.preview_scene_camera" 826 bl_label = "Preview Camera" 827 bl_description = "Preview chosen camera and make scene's active camera" 828 829 def execute(self, context): 830 chosen_camera = context.active_object 831 bpy.ops.view3d.object_as_camera() 832 bpy.ops.object.select_all(action="DESELECT") 833 chosen_camera.select_set(True) 834 return {'FINISHED'} 835 836 837class VIEW3D_OT_AddCameraMarker(Operator): 838 bl_idname = "cameraselector.add_camera_marker" 839 bl_label = "Add Camera Marker" 840 bl_description = "Add a timeline marker bound to chosen camera" 841 842 def execute(self, context): 843 chosen_camera = context.active_object 844 scene = context.scene 845 846 current_frame = scene.frame_current 847 marker = None 848 for m in reversed(sorted(filter(lambda m: m.frame <= current_frame, 849 scene.timeline_markers), 850 key=lambda m: m.frame)): 851 marker = m 852 break 853 if marker and (marker.camera == chosen_camera): 854 # Cancel if the last marker at or immediately before 855 # current frame is already bound to the camera. 856 return {'CANCELLED'} 857 858 marker_name = "F_%02d_%s" % (current_frame, chosen_camera.name) 859 if marker and (marker.frame == current_frame): 860 # Reuse existing marker at current frame to avoid 861 # overlapping bound markers. 862 marker.name = marker_name 863 else: 864 marker = scene.timeline_markers.new(marker_name) 865 marker.frame = scene.frame_current 866 marker.camera = chosen_camera 867 marker.select = True 868 869 for other_marker in [m for m in scene.timeline_markers if m != marker]: 870 other_marker.select = False 871 872 return {'FINISHED'} 873 874# gpl authors: nfloyd, Francesco Siddi 875 876 877 878 879 880# TODO: reinstate filters? 881class IO_Utils(): 882 883 @staticmethod 884 def get_preset_path(): 885 # locate stored_views preset folder 886 paths = bpy.utils.preset_paths("stored_views") 887 if not paths: 888 # stored_views preset folder doesn't exist, so create it 889 paths = [os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets", 890 "stored_views")] 891 if not os.path.exists(paths[0]): 892 os.makedirs(paths[0]) 893 894 return(paths) 895 896 @staticmethod 897 def stored_views_apply_from_scene(scene_name, replace=True): 898 scene = bpy.context.scene 899 scene_exists = True if scene_name in bpy.data.scenes.keys() else False 900 901 if scene_exists: 902 sv = bpy.context.scene.stored_views 903 # io_filters = sv.settings.io_filters 904 905 structs = [sv.view_list, sv.pov_list, sv.layers_list, sv.display_list] 906 if replace is True: 907 for st in structs: # clear swap and list 908 while len(st) > 0: 909 st.remove(0) 910 911 f_sv = bpy.data.scenes[scene_name].stored_views 912 # f_sv = bpy.data.scenes[scene_name].stored_views 913 f_structs = [f_sv.view_list, f_sv.pov_list, f_sv.layers_list, f_sv.display_list] 914 """ 915 is_filtered = [io_filters.views, io_filters.point_of_views, 916 io_filters.layers, io_filters.displays] 917 """ 918 for i in range(len(f_structs)): 919 """ 920 if is_filtered[i] is False: 921 continue 922 """ 923 for j in f_structs[i]: 924 item = structs[i].add() 925 # stored_views_copy_item(j, item) 926 for k, v in j.items(): 927 item[k] = v 928 DataStore.sanitize_data(scene) 929 return True 930 else: 931 return False 932 933 @staticmethod 934 def stored_views_export_to_blsv(filepath, name='Custom Preset'): 935 # create dictionary with all information 936 dump = {"info": {}, "data": {}} 937 dump["info"]["script"] = bl_info['name'] 938 dump["info"]["script_version"] = bl_info['version'] 939 dump["info"]["version"] = bpy.app.version 940 dump["info"]["preset_name"] = name 941 942 # get current stored views settings 943 scene = bpy.context.scene 944 sv = scene.stored_views 945 946 def dump_view_list(dict, list): 947 if str(type(list)) == "<class 'bpy_prop_collection_idprop'>": 948 for i, struct_dict in enumerate(list): 949 dict[i] = {"name": str, 950 "pov": {}, 951 "layers": {}, 952 "display": {}} 953 dict[i]["name"] = struct_dict.name 954 dump_item(dict[i]["pov"], struct_dict.pov) 955 dump_item(dict[i]["layers"], struct_dict.layers) 956 dump_item(dict[i]["display"], struct_dict.display) 957 958 def dump_list(dict, list): 959 if str(type(list)) == "<class 'bpy_prop_collection_idprop'>": 960 for i, struct in enumerate(list): 961 dict[i] = {} 962 dump_item(dict[i], struct) 963 964 def dump_item(dict, struct): 965 for prop in struct.bl_rna.properties: 966 if prop.identifier == "rna_type": 967 # not a setting, so skip 968 continue 969 970 val = getattr(struct, prop.identifier) 971 if str(type(val)) in ["<class 'bpy_prop_array'>"]: 972 # array 973 dict[prop.identifier] = [v for v in val] 974 # address the pickle limitations of dealing with the Vector class 975 elif str(type(val)) in ["<class 'Vector'>", 976 "<class 'Quaternion'>"]: 977 dict[prop.identifier] = [v for v in val] 978 else: 979 # single value 980 dict[prop.identifier] = val 981 982 # io_filters = sv.settings.io_filters 983 dump["data"] = {"point_of_views": {}, 984 "layers": {}, 985 "displays": {}, 986 "views": {}} 987 988 others_data = [(dump["data"]["point_of_views"], sv.pov_list), # , io_filters.point_of_views), 989 (dump["data"]["layers"], sv.layers_list), # , io_filters.layers), 990 (dump["data"]["displays"], sv.display_list)] # , io_filters.displays)] 991 for list_data in others_data: 992 # if list_data[2] is True: 993 dump_list(list_data[0], list_data[1]) 994 995 views_data = (dump["data"]["views"], sv.view_list) 996 # if io_filters.views is True: 997 dump_view_list(views_data[0], views_data[1]) 998 999 # save to file 1000 filepath = filepath 1001 filepath = bpy.path.ensure_ext(filepath, '.blsv') 1002 file = gzip.open(filepath, mode='wb') 1003 pickle.dump(dump, file, protocol=pickle.HIGHEST_PROTOCOL) 1004 file.close() 1005 1006 @staticmethod 1007 def stored_views_apply_preset(filepath, replace=True): 1008 if not filepath: 1009 return False 1010 1011 file = gzip.open(filepath, mode='rb') 1012 dump = pickle.load(file) 1013 file.close() 1014 # apply preset 1015 scene = bpy.context.scene 1016 sv = getattr(scene, "stored_views", None) 1017 1018 if not sv: 1019 return False 1020 1021 # io_filters = sv.settings.io_filters 1022 sv_data = { 1023 "point_of_views": sv.pov_list, 1024 "views": sv.view_list, 1025 "layers": sv.layers_list, 1026 "displays": sv.display_list 1027 } 1028 for sv_struct, props in dump["data"].items(): 1029 """ 1030 is_filtered = getattr(io_filters, sv_struct) 1031 if is_filtered is False: 1032 continue 1033 """ 1034 sv_list = sv_data[sv_struct] # .list 1035 if replace is True: # clear swap and list 1036 while len(sv_list) > 0: 1037 sv_list.remove(0) 1038 for key, prop_struct in props.items(): 1039 sv_item = sv_list.add() 1040 1041 for subprop, subval in prop_struct.items(): 1042 if isinstance(subval, dict): # views : pov, layers, displays 1043 v_subprop = getattr(sv_item, subprop) 1044 for v_subkey, v_subval in subval.items(): 1045 if isinstance(v_subval, list): # array like of pov,... 1046 v_array_like = getattr(v_subprop, v_subkey) 1047 for i in range(len(v_array_like)): 1048 v_array_like[i] = v_subval[i] 1049 else: 1050 setattr(v_subprop, v_subkey, v_subval) # others 1051 elif isinstance(subval, list): 1052 array_like = getattr(sv_item, subprop) 1053 for i in range(len(array_like)): 1054 array_like[i] = subval[i] 1055 else: 1056 setattr(sv_item, subprop, subval) 1057 1058 DataStore.sanitize_data(scene) 1059 1060 return True 1061 1062 1063class VIEW3D_OT_stored_views_import(Operator, ImportHelper): 1064 bl_idname = "stored_views.import_blsv" 1065 bl_label = "Import Stored Views preset" 1066 bl_description = "Import a .blsv preset file to the current Stored Views" 1067 1068 filename_ext = ".blsv" 1069 filter_glob: StringProperty( 1070 default="*.blsv", 1071 options={'HIDDEN'} 1072 ) 1073 replace: BoolProperty( 1074 name="Replace", 1075 default=True, 1076 description="Replace current stored views, otherwise append" 1077 ) 1078 1079 @classmethod 1080 def poll(cls, context): 1081 return get_preferences() 1082 1083 def execute(self, context): 1084 # the usual way is to not select the file in the file browser 1085 exists = os.path.isfile(self.filepath) if self.filepath else False 1086 if not exists: 1087 self.report({'WARNING'}, 1088 "No filepath specified or file could not be found. Operation Cancelled") 1089 return {'CANCELLED'} 1090 1091 # apply chosen preset 1092 apply_preset = IO_Utils.stored_views_apply_preset( 1093 filepath=self.filepath, replace=self.replace 1094 ) 1095 if not apply_preset: 1096 self.report({'WARNING'}, 1097 "Please Initialize Stored Views first (in the 3D View Properties Area)") 1098 return {'CANCELLED'} 1099 1100 # copy preset to presets folder 1101 filename = os.path.basename(self.filepath) 1102 try: 1103 shutil.copyfile(self.filepath, 1104 os.path.join(IO_Utils.get_preset_path()[0], filename)) 1105 except: 1106 self.report({'WARNING'}, 1107 "Stored Views: preset applied, but installing failed (preset already exists?)") 1108 return{'CANCELLED'} 1109 1110 return{'FINISHED'} 1111 1112 1113class VIEW3D_OT_stored_views_import_from_scene(Operator): 1114 bl_idname = "stored_views.import_from_scene" 1115 bl_label = "Import stored views from scene" 1116 bl_description = "Import currently stored views from an another scene" 1117 1118 scene_name: StringProperty( 1119 name="Scene Name", 1120 description="A current blend scene", 1121 default="" 1122 ) 1123 replace: BoolProperty( 1124 name="Replace", 1125 default=True, 1126 description="Replace current stored views, otherwise append" 1127 ) 1128 1129 @classmethod 1130 def poll(cls, context): 1131 return get_preferences() 1132 1133 def draw(self, context): 1134 layout = self.layout 1135 1136 layout.prop_search(self, "scene_name", bpy.data, "scenes") 1137 layout.prop(self, "replace") 1138 1139 def invoke(self, context, event): 1140 return context.window_manager.invoke_props_dialog(self) 1141 1142 def execute(self, context): 1143 # filepath should always be given 1144 if not self.scene_name: 1145 self.report({"WARNING"}, 1146 "No scene name was given. Operation Cancelled") 1147 return{'CANCELLED'} 1148 1149 is_finished = IO_Utils.stored_views_apply_from_scene( 1150 self.scene_name, replace=self.replace 1151 ) 1152 if not is_finished: 1153 self.report({"WARNING"}, 1154 "Could not find the specified scene. Operation Cancelled") 1155 return {"CANCELLED"} 1156 1157 return{'FINISHED'} 1158 1159 1160class VIEW3D_OT_stored_views_export(Operator, ExportHelper): 1161 bl_idname = "stored_views.export_blsv" 1162 bl_label = "Export Stored Views preset" 1163 bl_description = "Export the current Stored Views to a .blsv preset file" 1164 1165 filename_ext = ".blsv" 1166 filepath: StringProperty( 1167 default=os.path.join(IO_Utils.get_preset_path()[0], "untitled") 1168 ) 1169 filter_glob: StringProperty( 1170 default="*.blsv", 1171 options={'HIDDEN'} 1172 ) 1173 preset_name: StringProperty( 1174 name="Preset name", 1175 default="", 1176 description="Name of the stored views preset" 1177 ) 1178 1179 @classmethod 1180 def poll(cls, context): 1181 return get_preferences() 1182 1183 def execute(self, context): 1184 IO_Utils.stored_views_export_to_blsv(self.filepath, self.preset_name) 1185 1186 return{'FINISHED'} 1187 1188 1189class VIEW3D_PT_properties_stored_views(Panel): 1190 bl_label = "Stored Views" 1191 bl_space_type = "VIEW_3D" 1192 bl_region_type = "UI" 1193 bl_category = "View" 1194 1195 def draw(self, context): 1196 self.logger = logging.getLogger('%s Properties panel' % __name__) 1197 layout = self.layout 1198 1199 if bpy.ops.view3d.stored_views_initialize.poll(): 1200 layout.operator("view3d.stored_views_initialize") 1201 return 1202 1203 stored_views = context.scene.stored_views 1204 1205 # UI : mode 1206 col = layout.column(align=True) 1207 col.prop_enum(stored_views, "mode", 'VIEW') 1208 row = layout.row(align=True) 1209 row.operator("view3d.camera_to_view", text="Camera To view") 1210 row.operator("stored_views.newcamera") 1211 1212 row = col.row(align=True) 1213 row.prop_enum(stored_views, "mode", 'POV') 1214 row.prop_enum(stored_views, "mode", 'LAYERS') 1215 row.prop_enum(stored_views, "mode", 'DISPLAY') 1216 1217 # UI : operators 1218 row = layout.row() 1219 row.operator("stored_views.save").index = -1 1220 1221 # IO Operators 1222 if core.get_preferences(): 1223 row = layout.row(align=True) 1224 row.operator("stored_views.import_from_scene", text="Import from Scene") 1225 row.operator("stored_views.import_blsv", text="", icon="IMPORT") 1226 row.operator("stored_views.export_blsv", text="", icon="EXPORT") 1227 1228 data_store = DataStore() 1229 list = data_store.list 1230 # UI : items list 1231 if len(list) > 0: 1232 row = layout.row() 1233 box = row.box() 1234 # items list 1235 mode = stored_views.mode 1236 for i in range(len(list)): 1237 # associated icon 1238 icon_string = "MESH_CUBE" # default icon 1239 # TODO: icons for view 1240 if mode == 'POV': 1241 persp = list[i].perspective 1242 if persp == 'PERSP': 1243 icon_string = "MESH_CUBE" 1244 elif persp == 'ORTHO': 1245 icon_string = "MESH_PLANE" 1246 elif persp == 'CAMERA': 1247 if list[i].camera_type != 'CAMERA': 1248 icon_string = 'OBJECT_DATAMODE' 1249 else: 1250 icon_string = "OUTLINER_DATA_CAMERA" 1251 if mode == 'LAYERS': 1252 if list[i].lock_camera_and_layers is True: 1253 icon_string = 'SCENE_DATA' 1254 else: 1255 icon_string = 'RENDERLAYERS' 1256 if mode == 'DISPLAY': 1257 shade = list[i].viewport_shade 1258 if shade == 'TEXTURED': 1259 icon_string = 'TEXTURE_SHADED' 1260 if shade == 'MATERIAL': 1261 icon_string = 'MATERIAL_DATA' 1262 elif shade == 'SOLID': 1263 icon_string = 'SOLID' 1264 elif shade == 'WIREFRAME': 1265 icon_string = "WIRE" 1266 elif shade == 'BOUNDBOX': 1267 icon_string = 'BBOX' 1268 elif shade == 'RENDERED': 1269 icon_string = 'MATERIAL' 1270 # stored view row 1271 subrow = box.row(align=True) 1272 # current view indicator 1273 if data_store.current_index == i and context.scene.stored_views.view_modified is False: 1274 subrow.label(text="", icon='SMALL_TRI_RIGHT_VEC') 1275 subrow.operator("stored_views.set", 1276 text="", icon=icon_string).index = i 1277 subrow.prop(list[i], "name", text="") 1278 subrow.operator("stored_views.save", 1279 text="", icon="REC").index = i 1280 subrow.operator("stored_views.delete", 1281 text="", icon="PANEL_CLOSE").index = i 1282 1283 layout = self.layout 1284 scene = context.scene 1285 layout.label(text="Camera Selector") 1286 cameras = sorted([o for o in scene.objects if o.type == 'CAMERA'], 1287 key=lambda o: o.name) 1288 1289 if len(cameras) > 0: 1290 for camera in cameras: 1291 row = layout.row(align=True) 1292 row.context_pointer_set("active_object", camera) 1293 row.operator("cameraselector.set_scene_camera", 1294 text=camera.name, icon='OUTLINER_DATA_CAMERA') 1295 row.operator("cameraselector.preview_scene_camera", 1296 text='', icon='RESTRICT_VIEW_OFF') 1297 row.operator("cameraselector.add_camera_marker", 1298 text='', icon='MARKER') 1299 else: 1300 layout.label(text="No cameras in this scene") 1301# Addon Preferences 1302 1303class VIEW3D_OT_stored_views_preferences(AddonPreferences): 1304 bl_idname = __name__ 1305 1306 show_exporters: BoolProperty( 1307 name="Enable I/O Operators", 1308 default=False, 1309 description="Enable Import/Export Operations in the UI:\n" 1310 "Import Stored Views preset,\n" 1311 "Export Stored Views preset and \n" 1312 "Import stored views from scene", 1313 ) 1314 view_3d_update_rate: IntProperty( 1315 name="3D view update", 1316 description="Update rate of the 3D view redraw\n" 1317 "Increse the value if the UI feels sluggish", 1318 min=1, max=10, 1319 default=1 1320 ) 1321 1322 def draw(self, context): 1323 layout = self.layout 1324 1325 row = layout.row(align=True) 1326 row.prop(self, "view_3d_update_rate", toggle=True) 1327 row.prop(self, "show_exporters", toggle=True) 1328 1329 1330# Register 1331classes = [ 1332 VIEW3D_OT_stored_views_initialize, 1333 VIEW3D_OT_stored_views_preferences, 1334 VIEW3D_PT_properties_stored_views, 1335 POVData, 1336 LayersData, 1337 DisplayData, 1338 ViewData, 1339 StoredViewsData, 1340 VIEW3D_OT_stored_views_draw, 1341 VIEW3D_OT_stored_views_save, 1342 VIEW3D_OT_stored_views_set, 1343 VIEW3D_OT_stored_views_delete, 1344 VIEW3D_OT_New_Camera_to_View, 1345 VIEW3D_OT_SetSceneCamera, 1346 VIEW3D_OT_PreviewSceneCamera, 1347 VIEW3D_OT_AddCameraMarker, 1348# IO_Utils, 1349 VIEW3D_OT_stored_views_import, 1350 VIEW3D_OT_stored_views_import_from_scene, 1351 VIEW3D_OT_stored_views_export 1352 ] 1353 1354 1355def register(): 1356 from bpy.utils import register_class 1357 for cls in classes: 1358 register_class(cls) 1359 1360 1361def unregister(): 1362 ui.VIEW3D_OT_stored_views_draw.handle_remove(bpy.context) 1363 from bpy.utils import unregister_class 1364 for cls in reversed(classes): 1365 unregister_class(cls) 1366 if hasattr(bpy.types.Scene, "stored_views"): 1367 del bpy.types.Scene.stored_views 1368 1369 1370if __name__ == "__main__": 1371 register() 1372