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