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