Cleanup: simplify file name incrementing logic
[blender-addons.git] / space_view3d_stored_views / stored_views_test.py
blob19b2a0efa4b4ede64d4384775082b32eabb21325
1 # ##### BEGIN GPL LICENSE BLOCK #####
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.
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.
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.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_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"
32 """
33 ACKNOWLEDGMENT
34 ==============
35 import/export functionality is mostly based
36 on Bart Crouch's Theme Manager Addon
38 TODO: quadview complete support : investigate. Where's the data?
39 TODO: lock_camera_and_layers. investigate usage
40 TODO: list reordering
42 NOTE: 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 """
54 import bpy
55 from bpy.props import (
56 BoolProperty,
57 IntProperty,
58 PointerProperty,
60 from bpy.types import (
61 AddonPreferences,
62 Operator,
63 Panel
66 import logging
67 module_logger = logging.getLogger(__name__)
69 import gzip
70 import os
71 import pickle
72 import shutil
74 from bpy_extras.io_utils import (
75 ExportHelper,
76 ImportHelper,
79 import blf
81 import hashlib
82 import bpy
85 # Utility function get preferences setting for exporters
86 def 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)
91 return show_warn
94 class 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)
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))
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))
115 def from_v3d(self, stored_view):
116 raise NotImplementedError("Subclass must implement abstract method")
118 def update_v3d(self, stored_view):
119 raise NotImplementedError("Subclass must implement abstract method")
121 @staticmethod
122 def is_modified(context, stored_view):
123 raise NotImplementedError("Subclass must implement abstract method")
126 class POV(StoredView):
127 def __init__(self, index=None):
128 super().__init__(mode='POV', index=index)
129 self.logger = logging.getLogger('%s.POV' % __name__)
131 def from_v3d(self, stored_view):
132 view3d = self.view3d
133 region3d = view3d.region_3d
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
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
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
169 if stored_view.perspective == "CAMERA":
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
179 @staticmethod
180 def _get_object(name, pointer=None):
181 return bpy.data.objects.get(name)
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
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
200 return False
202 @staticmethod
203 def _get_perspective_matrix_md5(region3d):
204 md5 = hashlib.md5(str(region3d.perspective_matrix).encode('utf-8')).hexdigest()
205 return md5
208 class Layers(StoredView):
209 def __init__(self, index=None):
210 super().__init__(mode='LAYERS', index=index)
211 self.logger = logging.getLogger('%s.Layers' % __name__)
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
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
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
245 class Display(StoredView):
246 def __init__(self, index=None):
247 super().__init__(mode='DISPLAY', index=index)
248 self.logger = logging.getLogger('%s.Display' % __name__)
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
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
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
294 if stored_view.material_mode != context.scene.game_settings.material_mode:
295 Display.logger.debug('material_mode')
296 return True
299 class 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()
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)
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)
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
326 class 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
333 if mode is None:
334 self.mode = stored_views.mode
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]
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
356 def get(self, index):
357 self._set_current_index(index)
358 return self.list[index]
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)
366 self.list.remove(index)
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
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
401 @staticmethod
402 def sanitize_data(scene):
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
412 if item.perspective == "CAMERA":
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
422 obj = bpy.data.objects.get(item.lock_object_name)
423 if obj is None and camera is None:
424 to_remove.append(i)
426 for i in reversed(to_remove):
427 list.remove(i)
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)
435 def 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)
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
455 # Utility function get_preferences_timer for update of 3d view draw
456 def 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)
462 return timer_update
465 def init_draw(context=None):
466 if context is None:
467 context = bpy.context
469 if "stored_views_osd" not in context.window_manager:
470 context.window_manager["stored_views_osd"] = False
472 if not context.window_manager["stored_views_osd"]:
473 context.window_manager["stored_views_osd"] = True
474 bpy.ops.stored_views.draw()
477 def _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
483 blf.size(font_id, 11, context.preferences.system.dpi)
484 text_size = blf.dimensions(0, self.view_name)
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
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)
500 class 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"
505 _handle = None
506 _timer = None
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)
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
524 @classmethod
525 def poll(cls, context):
526 # return context.mode == 'OBJECT'
527 return True
529 def modal(self, context, event):
530 if context.area:
531 context.area.tag_redraw()
533 if not context.area or context.area.type != "VIEW_3D":
534 return {"PASS_THROUGH"}
536 data = DataStore()
537 stored_views = context.scene.stored_views
539 if len(data.list) > 0 and \
540 data.current_index >= 0 and \
541 not stored_views.view_modified:
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)
560 self.view_name = ""
561 stored_views.view_modified = is_modified
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)
569 return {'FINISHED'}
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)
577 return {"RUNNING_MODAL"}
578 else:
579 self.report({"WARNING"}, "View3D not found. Operation Cancelled")
581 return {"CANCELLED"}
583 class VIEW3D_OT_stored_views_initialize(Operator):
584 bl_idname = "view3d.stored_views_initialize"
585 bl_label = "Initialize"
587 @classmethod
588 def poll(cls, context):
589 return not hasattr(bpy.types.Scene, 'stored_views')
591 def execute(self, context):
592 bpy.types.Scene.stored_views: PointerProperty(
593 type=properties.StoredViewsData
595 scenes = bpy.data.scenes
596 data = DataStore()
597 for scene in scenes:
598 DataStore.sanitize_data(scene)
599 return {'FINISHED'}
602 from bpy.types import PropertyGroup
603 from bpy.props import (
604 BoolProperty,
605 BoolVectorProperty,
606 CollectionProperty,
607 FloatProperty,
608 FloatVectorProperty,
609 EnumProperty,
610 IntProperty,
611 IntVectorProperty,
612 PointerProperty,
613 StringProperty,
617 class POVData(PropertyGroup):
618 distance: FloatProperty()
619 location: FloatVectorProperty(
620 subtype='TRANSLATION'
622 rotation: FloatVectorProperty(
623 subtype='QUATERNION',
624 size=4
626 name: StringProperty()
627 perspective: EnumProperty(
628 items=[('PERSP', '', ''),
629 ('ORTHO', '', ''),
630 ('CAMERA', '', '')]
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()
643 class LayersData(PropertyGroup):
644 view_layers: BoolVectorProperty(size=20)
645 scene_layers: BoolVectorProperty(size=20)
646 lock_camera_and_layers: BoolProperty()
647 name: StringProperty()
650 class 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')]
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()
679 class ViewData(PropertyGroup):
680 pov: PointerProperty(
681 type=POVData
683 layers: PointerProperty(
684 type=LayersData
686 display: PointerProperty(
687 type=DisplayData
689 name: StringProperty()
692 class StoredViewsData(PropertyGroup):
693 pov_list: CollectionProperty(
694 type=POVData
696 layers_list: CollectionProperty(
697 type=LayersData
699 display_list: CollectionProperty(
700 type=DisplayData
702 view_list: CollectionProperty(
703 type=ViewData
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'
713 current_indices: IntVectorProperty(
714 size=4,
715 default=[-1, -1, -1, -1]
717 view_modified: BoolProperty(
718 default=False
721 class 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"
726 index: IntProperty()
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)
735 return {'FINISHED'}
738 class 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"
743 index: IntProperty()
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)
752 return {'FINISHED'}
755 class VIEW3D_OT_stored_views_delete(Operator):
756 bl_idname = "stored_views.delete"
757 bl_label = "Delete"
758 bl_description = "Delete this view"
760 index: IntProperty()
762 def execute(self, context):
763 data = DataStore()
764 data.delete(self.index)
766 return {'FINISHED'}
769 class 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"
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'
782 def execute(self, context):
784 if bpy.ops.object.mode_set.poll():
785 bpy.ops.object.mode_set(mode='OBJECT')
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
793 bpy.ops.view3d.camera_to_view()
794 return {'FINISHED'}
797 # Camera marker & switcher by Fsiddi
798 class 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"
803 hide_others = False
805 def execute(self, context):
806 chosen_camera = context.active_object
807 scene = context.scene
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'}
817 def invoke(self, context, event):
818 if event.ctrl:
819 self.hide_others = True
821 return self.execute(context)
824 class 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"
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'}
837 class 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"
842 def execute(self, context):
843 chosen_camera = context.active_object
844 scene = context.scene
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'}
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
869 for other_marker in [m for m in scene.timeline_markers if m != marker]:
870 other_marker.select = False
872 return {'FINISHED'}
874 # gpl authors: nfloyd, Francesco Siddi
880 # TODO: reinstate filters?
881 class IO_Utils():
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])
894 return(paths)
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
901 if scene_exists:
902 sv = bpy.context.scene.stored_views
903 # io_filters = sv.settings.io_filters
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)
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]
915 is_filtered = [io_filters.views, io_filters.point_of_views,
916 io_filters.layers, io_filters.displays]
918 for i in range(len(f_structs)):
920 if is_filtered[i] is False:
921 continue
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
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
942 # get current stored views settings
943 scene = bpy.context.scene
944 sv = scene.stored_views
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)
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)
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
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
982 # io_filters = sv.settings.io_filters
983 dump["data"] = {"point_of_views": {},
984 "layers": {},
985 "displays": {},
986 "views": {}}
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])
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])
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()
1006 @staticmethod
1007 def stored_views_apply_preset(filepath, replace=True):
1008 if not filepath:
1009 return False
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)
1018 if not sv:
1019 return False
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
1028 for sv_struct, props in dump["data"].items():
1030 is_filtered = getattr(io_filters, sv_struct)
1031 if is_filtered is False:
1032 continue
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()
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)
1058 DataStore.sanitize_data(scene)
1060 return True
1063 class 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"
1068 filename_ext = ".blsv"
1069 filter_glob: StringProperty(
1070 default="*.blsv",
1071 options={'HIDDEN'}
1073 replace: BoolProperty(
1074 name="Replace",
1075 default=True,
1076 description="Replace current stored views, otherwise append"
1079 @classmethod
1080 def poll(cls, context):
1081 return get_preferences()
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'}
1091 # apply chosen preset
1092 apply_preset = IO_Utils.stored_views_apply_preset(
1093 filepath=self.filepath, replace=self.replace
1095 if not apply_preset:
1096 self.report({'WARNING'},
1097 "Please Initialize Stored Views first (in the 3D View Properties Area)")
1098 return {'CANCELLED'}
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'}
1110 return{'FINISHED'}
1113 class 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"
1118 scene_name: StringProperty(
1119 name="Scene Name",
1120 description="A current blend scene",
1121 default=""
1123 replace: BoolProperty(
1124 name="Replace",
1125 default=True,
1126 description="Replace current stored views, otherwise append"
1129 @classmethod
1130 def poll(cls, context):
1131 return get_preferences()
1133 def draw(self, context):
1134 layout = self.layout
1136 layout.prop_search(self, "scene_name", bpy.data, "scenes")
1137 layout.prop(self, "replace")
1139 def invoke(self, context, event):
1140 return context.window_manager.invoke_props_dialog(self)
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'}
1149 is_finished = IO_Utils.stored_views_apply_from_scene(
1150 self.scene_name, replace=self.replace
1152 if not is_finished:
1153 self.report({"WARNING"},
1154 "Could not find the specified scene. Operation Cancelled")
1155 return {"CANCELLED"}
1157 return{'FINISHED'}
1160 class 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"
1165 filename_ext = ".blsv"
1166 filepath: StringProperty(
1167 default=os.path.join(IO_Utils.get_preset_path()[0], "untitled")
1169 filter_glob: StringProperty(
1170 default="*.blsv",
1171 options={'HIDDEN'}
1173 preset_name: StringProperty(
1174 name="Preset name",
1175 default="",
1176 description="Name of the stored views preset"
1179 @classmethod
1180 def poll(cls, context):
1181 return get_preferences()
1183 def execute(self, context):
1184 IO_Utils.stored_views_export_to_blsv(self.filepath, self.preset_name)
1186 return{'FINISHED'}
1189 class 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"
1195 def draw(self, context):
1196 self.logger = logging.getLogger('%s Properties panel' % __name__)
1197 layout = self.layout
1199 if bpy.ops.view3d.stored_views_initialize.poll():
1200 layout.operator("view3d.stored_views_initialize")
1201 return
1203 stored_views = context.scene.stored_views
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")
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')
1217 # UI : operators
1218 row = layout.row()
1219 row.operator("stored_views.save").index = -1
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")
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
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)
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
1303 class VIEW3D_OT_stored_views_preferences(AddonPreferences):
1304 bl_idname = __name__
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",
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
1322 def draw(self, context):
1323 layout = self.layout
1325 row = layout.row(align=True)
1326 row.prop(self, "view_3d_update_rate", toggle=True)
1327 row.prop(self, "show_exporters", toggle=True)
1330 # Register
1331 classes = [
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
1355 def register():
1356 from bpy.utils import register_class
1357 for cls in classes:
1358 register_class(cls)
1361 def 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
1370 if __name__ == "__main__":
1371 register()