1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Stored Views",
5 "description": "Save and restore User defined views, pov, layers and display configs",
6 "author": "nfloyd, Francesco Siddi",
9 "location": "View3D > Properties > Stored Views",
11 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.5/"
12 "Py/Scripts/3D_interaction/stored_views",
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
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
39 from bpy
.props
import (
44 from bpy
.types
import (
51 module_logger
= logging
.getLogger(__name__
)
58 from bpy_extras
.io_utils
import (
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)
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
84 self
.data_store
= DataStore(mode
=mode
)
88 stored_view
, self
.index
= self
.data_store
.create()
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
))
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")
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
):
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
_md
5(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
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
):
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
)
157 view3d
.lock_object
= lock_obj
159 cam
= self
._get
_object
(stored_view
.camera_name
)
164 def _get_object(name
, pointer
=None):
165 return bpy
.data
.objects
.get(name
)
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')
178 md5
= POV
._get
_perspective
_matrix
_md
5(region3d
)
179 if (md5
!= stored_view
.perspective_matrix_md5
and
180 region3d
.view_perspective
!= "CAMERA"):
181 POV
.logger
.debug('perspective_matrix')
187 def _get_perspective_matrix_md5(region3d
):
188 md5
= hashlib
.md5(str(region3d
.perspective_matrix
).encode('utf-8')).hexdigest()
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
):
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
):
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
209 view3d
.layers
= stored_view
.view_layers
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')
217 if stored_view
.lock_camera_and_layers
is True:
219 if stored_view
.scene_layers
[i
] != context
.scene
.layers
[i
]:
220 Layers
.logger
.debug('scene_layers[%s]' % (i
, ))
224 if stored_view
.view_layers
[i
] != context
.space_data
.view3d
.layers
[i
]:
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
):
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
):
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
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
):
278 if stored_view
.material_mode
!= context
.scene
.game_settings
.material_mode
:
279 Display
.logger
.debug('material_mode')
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__
)
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
)
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
):
311 def __init__(self
, scene
=None, mode
=None):
313 scene
= bpy
.context
.scene
314 stored_views
= scene
.stored_views
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]
334 item
= self
.list.add()
335 item
.name
= self
._generate
_name
()
336 index
= len(self
.list) - 1
337 self
._set
_current
_index
(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
355 stored_views
= bpy
.context
.scene
.stored_views
357 stored_views
.current_indices
[0] = index
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
)
370 if i_name
.startswith(default_name
):
375 post_fix
= l_name
.rpartition('.')[2]
376 if post_fix
.isnumeric():
377 post_fix
= str(int(post_fix
) + 1).zfill(3)
379 if post_fix
== default_name
:
381 return default_name
+ "." + post_fix
386 def sanitize_data(scene
):
388 def check_objects_references(mode
, list):
390 for i
, list_item
in enumerate(list.items()):
391 key
, item
= list_item
392 if mode
== 'POV' or mode
== 'VIEWS':
396 if item
.perspective
== "CAMERA":
398 camera
= bpy
.data
.objects
.get(item
.camera_name
)
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
406 obj
= bpy
.data
.objects
.get(item
.lock_object_name
)
407 if obj
is None and camera
is None:
410 for i
in reversed(to_remove
):
413 modes
= ['POV', 'VIEW', 'DISPLAY', 'LAYERS']
415 data
= DataStore(scene
=scene
, mode
=mode
)
416 check_objects_references(mode
, data
.list)
419 def stored_view_factory(mode
, *args
, **kwargs
):
421 return POV(*args
, **kwargs
)
422 elif mode
== 'LAYERS':
423 return Layers(*args
, **kwargs
)
424 elif mode
== 'DISPLAY':
425 return Display(*args
, **kwargs
)
427 return View(*args
, **kwargs
)
430 If view name display is enabled,
431 it will check periodically if the view has been modified
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)
449 def init_draw(context
=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
):
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
473 overlap
= context
.preferences
.system
.use_region_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"
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
)
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
510 def poll(cls
, context
):
511 # return context.mode == 'OBJECT'
514 def modal(self
, context
, event
):
516 context
.area
.tag_redraw()
518 if not context
.area
or context
.area
.type != "VIEW_3D":
519 return {"PASS_THROUGH"}
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':
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
)
543 'view modified - index: %s name: %s' % (data
.current_index
, sv
.name
)
546 stored_views
.view_modified
= is_modified
548 return {"PASS_THROUGH"}
550 module_logger
.debug('exit')
551 context
.window_manager
["stored_views_osd"] = False
552 VIEW3D_OT_stored_views_draw
.handle_remove(context
)
556 def execute(self
, context
):
557 if context
.area
.type == "VIEW_3D":
559 VIEW3D_OT_stored_views_draw
.handle_add(self
, context
)
560 context
.window_manager
.modal_handler_add(self
)
562 return {"RUNNING_MODAL"}
564 self
.report({"WARNING"}, "View3D not found. Operation Cancelled")
568 class VIEW3D_OT_stored_views_initialize(Operator
):
569 bl_idname
= "view3d.stored_views_initialize"
570 bl_label
= "Initialize"
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
583 DataStore
.sanitize_data(scene
)
587 from bpy
.types
import PropertyGroup
588 from bpy
.props
import (
602 class POVData(PropertyGroup
):
603 distance
: FloatProperty()
604 location
: FloatVectorProperty(
605 subtype
='TRANSLATION'
607 rotation
: FloatVectorProperty(
608 subtype
='QUATERNION',
611 name
: StringProperty()
612 perspective
: EnumProperty(
613 items
=[('PERSP', '', ''),
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(
668 layers
: PointerProperty(
671 display
: PointerProperty(
674 name
: StringProperty()
677 class StoredViewsData(PropertyGroup
):
678 pov_list
: CollectionProperty(
681 layers_list
: CollectionProperty(
684 display_list
: CollectionProperty(
687 view_list
: CollectionProperty(
692 items
=[('VIEW', "View", "3D View settings"),
693 ('POV', "POV", "POV settings"),
694 ('LAYERS', "Layers", "Layers settings"),
695 ('DISPLAY', "Display", "Display settings")],
698 current_indices
: IntVectorProperty(
700 default
=[-1, -1, -1, -1]
702 view_modified
: BoolProperty(
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"
713 def execute(self
, context
):
714 mode
= context
.scene
.stored_views
.mode
715 sv
= stored_view_factory(mode
, self
.index
)
717 context
.scene
.stored_views
.view_modified
= False
723 class VIEW3D_OT_stored_views_set(Operator
):
724 bl_idname
= "stored_views.set"
726 bl_description
= "Update the view 3D according to this view"
730 def execute(self
, context
):
731 mode
= context
.scene
.stored_views
.mode
732 sv
= stored_view_factory(mode
, self
.index
)
734 context
.scene
.stored_views
.view_modified
= False
740 class VIEW3D_OT_stored_views_delete(Operator
):
741 bl_idname
= "stored_views.delete"
743 bl_description
= "Delete this view"
747 def execute(self
, context
):
749 data
.delete(self
.index
)
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"
760 def poll(cls
, context
):
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()
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"
790 def execute(self
, context
):
791 chosen_camera
= context
.active_object
792 scene
= context
.scene
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)
802 def invoke(self
, context
, event
):
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)
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
833 for m
in reversed(sorted(filter(lambda m
: m
.frame
<= current_frame
,
834 scene
.timeline_markers
),
835 key
=lambda m
: m
.frame
)):
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.
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
849 marker
= scene
.timeline_markers
.new(marker_name
)
850 marker
.frame
= scene
.frame_current
851 marker
.camera
= chosen_camera
854 for other_marker
in [m
for m
in scene
.timeline_markers
if m
!= marker
]:
855 other_marker
.select
= False
859 # gpl authors: nfloyd, Francesco Siddi
865 # TODO: reinstate filters?
869 def get_preset_path():
870 # locate stored_views preset folder
871 paths
= bpy
.utils
.preset_paths("stored_views")
873 # stored_views preset folder doesn't exist, so create it
874 paths
= [os
.path
.join(bpy
.utils
.user_resource('SCRIPTS'), "presets",
876 if not os
.path
.exists(paths
[0]):
877 os
.makedirs(paths
[0])
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
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
]
892 for st
in structs
: # clear swap and list
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:
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():
913 DataStore
.sanitize_data(scene
)
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,
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):
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
955 val
= getattr(struct
, prop
.identifier
)
956 if str(type(val
)) in ["<class 'bpy_prop_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
]
965 dict[prop
.identifier
] = val
967 # io_filters = sv.settings.io_filters
968 dump
["data"] = {"point_of_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])
986 filepath
= bpy
.path
.ensure_ext(filepath
, '.blsv')
987 file = gzip
.open(filepath
, mode
='wb')
988 pickle
.dump(dump
, file, protocol
=pickle
.HIGHEST_PROTOCOL
)
992 def stored_views_apply_preset(filepath
, replace
=True):
996 file = gzip
.open(filepath
, mode
='rb')
997 dump
= pickle
.load(file)
1000 scene
= bpy
.context
.scene
1001 sv
= getattr(scene
, "stored_views", None)
1006 # io_filters = sv.settings.io_filters
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:
1019 sv_list
= sv_data
[sv_struct
] # .list
1020 if replace
is True: # clear swap and list
1021 while len(sv_list
) > 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
]
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
]
1041 setattr(sv_item
, subprop
, subval
)
1043 DataStore
.sanitize_data(scene
)
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(
1058 replace
: BoolProperty(
1061 description
="Replace current stored views, otherwise append"
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
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
)
1088 shutil
.copyfile(self
.filepath
,
1089 os
.path
.join(IO_Utils
.get_preset_path()[0], filename
))
1091 self
.report({'WARNING'},
1092 "Stored Views: preset applied, but installing failed (preset already exists?)")
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(
1105 description
="A current blend scene",
1108 replace
: BoolProperty(
1111 description
="Replace current stored views, otherwise append"
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")
1134 is_finished
= IO_Utils
.stored_views_apply_from_scene(
1135 self
.scene_name
, replace
=self
.replace
1138 self
.report({"WARNING"},
1139 "Could not find the specified scene. Operation Cancelled")
1140 return {"CANCELLED"}
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(
1158 preset_name
: StringProperty(
1161 description
="Name of the stored views preset"
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
)
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")
1188 stored_views
= context
.scene
.stored_views
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')
1204 row
.operator("stored_views.save").index
= -1
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
1220 mode
= stored_views
.mode
1221 for i
in range(len(list)):
1223 icon_string
= "MESH_CUBE" # default icon
1224 # TODO: icons for view
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'
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'
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'
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')
1285 layout
.label(text
="No cameras in this scene")
1288 class VIEW3D_OT_stored_views_preferences(AddonPreferences
):
1289 bl_idname
= __name__
1291 show_exporters
: BoolProperty(
1292 name
="Enable I/O Operators",
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",
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)
1317 VIEW3D_OT_stored_views_initialize
,
1318 VIEW3D_OT_stored_views_preferences
,
1319 VIEW3D_PT_properties_stored_views
,
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
,
1334 VIEW3D_OT_stored_views_import
,
1335 VIEW3D_OT_stored_views_import_from_scene
,
1336 VIEW3D_OT_stored_views_export
1341 from bpy
.utils
import register_class
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__":