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 #####
20 "name": "Stored Views",
21 "description": "Save and restore User defined views, pov, layers and display configs",
22 "author": "nfloyd, Francesco Siddi",
24 "blender": (2, 80, 0),
25 "location": "View3D > Properties > Stored Views",
27 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.5/"
28 "Py/Scripts/3D_interaction/stored_views",
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
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
55 from bpy
.props
import (
60 from bpy
.types
import (
67 module_logger
= logging
.getLogger(__name__
)
74 from bpy_extras
.io_utils
import (
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)
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
100 self
.data_store
= DataStore(mode
=mode
)
104 stored_view
, self
.index
= self
.data_store
.create()
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
))
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")
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
):
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
_md
5(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
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
):
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
)
173 view3d
.lock_object
= lock_obj
175 cam
= self
._get
_object
(stored_view
.camera_name
)
180 def _get_object(name
, pointer
=None):
181 return bpy
.data
.objects
.get(name
)
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')
194 md5
= POV
._get
_perspective
_matrix
_md
5(region3d
)
195 if (md5
!= stored_view
.perspective_matrix_md5
and
196 region3d
.view_perspective
!= "CAMERA"):
197 POV
.logger
.debug('perspective_matrix')
203 def _get_perspective_matrix_md5(region3d
):
204 md5
= hashlib
.md5(str(region3d
.perspective_matrix
).encode('utf-8')).hexdigest()
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
):
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
):
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
225 view3d
.layers
= stored_view
.view_layers
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')
233 if stored_view
.lock_camera_and_layers
is True:
235 if stored_view
.scene_layers
[i
] != context
.scene
.layers
[i
]:
236 Layers
.logger
.debug('scene_layers[%s]' % (i
, ))
240 if stored_view
.view_layers
[i
] != context
.space_data
.view3d
.layers
[i
]:
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
):
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
):
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
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
):
294 if stored_view
.material_mode
!= context
.scene
.game_settings
.material_mode
:
295 Display
.logger
.debug('material_mode')
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__
)
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
)
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
):
327 def __init__(self
, scene
=None, mode
=None):
329 scene
= bpy
.context
.scene
330 stored_views
= scene
.stored_views
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]
350 item
= self
.list.add()
351 item
.name
= self
._generate
_name
()
352 index
= len(self
.list) - 1
353 self
._set
_current
_index
(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
371 stored_views
= bpy
.context
.scene
.stored_views
373 stored_views
.current_indices
[0] = index
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
)
386 if i_name
.startswith(default_name
):
391 post_fix
= l_name
.rpartition('.')[2]
392 if post_fix
.isnumeric():
393 post_fix
= str(int(post_fix
) + 1).zfill(3)
395 if post_fix
== default_name
:
397 return default_name
+ "." + post_fix
402 def sanitize_data(scene
):
404 def check_objects_references(mode
, list):
406 for i
, list_item
in enumerate(list.items()):
407 key
, item
= list_item
408 if mode
== 'POV' or mode
== 'VIEWS':
412 if item
.perspective
== "CAMERA":
414 camera
= bpy
.data
.objects
.get(item
.camera_name
)
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
422 obj
= bpy
.data
.objects
.get(item
.lock_object_name
)
423 if obj
is None and camera
is None:
426 for i
in reversed(to_remove
):
429 modes
= ['POV', 'VIEW', 'DISPLAY', 'LAYERS']
431 data
= DataStore(scene
=scene
, mode
=mode
)
432 check_objects_references(mode
, data
.list)
435 def stored_view_factory(mode
, *args
, **kwargs
):
437 return POV(*args
, **kwargs
)
438 elif mode
== 'LAYERS':
439 return Layers(*args
, **kwargs
)
440 elif mode
== 'DISPLAY':
441 return Display(*args
, **kwargs
)
443 return View(*args
, **kwargs
)
446 If view name display is enabled,
447 it will check periodically if the view has been modified
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)
465 def init_draw(context
=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
488 overlap
= context
.preferences
.system
.use_region_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"
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
)
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
525 def poll(cls
, context
):
526 # return context.mode == 'OBJECT'
529 def modal(self
, context
, event
):
531 context
.area
.tag_redraw()
533 if not context
.area
or context
.area
.type != "VIEW_3D":
534 return {"PASS_THROUGH"}
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':
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
)
558 'view modified - index: %s name: %s' % (data
.current_index
, sv
.name
)
561 stored_views
.view_modified
= is_modified
563 return {"PASS_THROUGH"}
565 module_logger
.debug('exit')
566 context
.window_manager
["stored_views_osd"] = False
567 VIEW3D_OT_stored_views_draw
.handle_remove(context
)
571 def execute(self
, context
):
572 if context
.area
.type == "VIEW_3D":
574 VIEW3D_OT_stored_views_draw
.handle_add(self
, context
)
575 context
.window_manager
.modal_handler_add(self
)
577 return {"RUNNING_MODAL"}
579 self
.report({"WARNING"}, "View3D not found. Operation Cancelled")
583 class VIEW3D_OT_stored_views_initialize(Operator
):
584 bl_idname
= "view3d.stored_views_initialize"
585 bl_label
= "Initialize"
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
598 DataStore
.sanitize_data(scene
)
602 from bpy
.types
import PropertyGroup
603 from bpy
.props
import (
617 class POVData(PropertyGroup
):
618 distance
: FloatProperty()
619 location
: FloatVectorProperty(
620 subtype
='TRANSLATION'
622 rotation
: FloatVectorProperty(
623 subtype
='QUATERNION',
626 name
: StringProperty()
627 perspective
: EnumProperty(
628 items
=[('PERSP', '', ''),
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(
683 layers
: PointerProperty(
686 display
: PointerProperty(
689 name
: StringProperty()
692 class StoredViewsData(PropertyGroup
):
693 pov_list
: CollectionProperty(
696 layers_list
: CollectionProperty(
699 display_list
: CollectionProperty(
702 view_list
: CollectionProperty(
707 items
=[('VIEW', "View", "3D View settings"),
708 ('POV', "POV", "POV settings"),
709 ('LAYERS', "Layers", "Layers settings"),
710 ('DISPLAY', "Display", "Display settings")],
713 current_indices
: IntVectorProperty(
715 default
=[-1, -1, -1, -1]
717 view_modified
: BoolProperty(
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"
728 def execute(self
, context
):
729 mode
= context
.scene
.stored_views
.mode
730 sv
= stored_view_factory(mode
, self
.index
)
732 context
.scene
.stored_views
.view_modified
= False
738 class VIEW3D_OT_stored_views_set(Operator
):
739 bl_idname
= "stored_views.set"
741 bl_description
= "Update the view 3D according to this view"
745 def execute(self
, context
):
746 mode
= context
.scene
.stored_views
.mode
747 sv
= stored_view_factory(mode
, self
.index
)
749 context
.scene
.stored_views
.view_modified
= False
755 class VIEW3D_OT_stored_views_delete(Operator
):
756 bl_idname
= "stored_views.delete"
758 bl_description
= "Delete this view"
762 def execute(self
, context
):
764 data
.delete(self
.index
)
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"
775 def poll(cls
, context
):
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()
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"
805 def execute(self
, context
):
806 chosen_camera
= context
.active_object
807 scene
= context
.scene
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)
817 def invoke(self
, context
, event
):
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)
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
848 for m
in reversed(sorted(filter(lambda m
: m
.frame
<= current_frame
,
849 scene
.timeline_markers
),
850 key
=lambda m
: m
.frame
)):
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.
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
864 marker
= scene
.timeline_markers
.new(marker_name
)
865 marker
.frame
= scene
.frame_current
866 marker
.camera
= chosen_camera
869 for other_marker
in [m
for m
in scene
.timeline_markers
if m
!= marker
]:
870 other_marker
.select
= False
874 # gpl authors: nfloyd, Francesco Siddi
880 # TODO: reinstate filters?
884 def get_preset_path():
885 # locate stored_views preset folder
886 paths
= bpy
.utils
.preset_paths("stored_views")
888 # stored_views preset folder doesn't exist, so create it
889 paths
= [os
.path
.join(bpy
.utils
.user_resource('SCRIPTS'), "presets",
891 if not os
.path
.exists(paths
[0]):
892 os
.makedirs(paths
[0])
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
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
]
907 for st
in structs
: # clear swap and list
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:
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():
928 DataStore
.sanitize_data(scene
)
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,
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):
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
970 val
= getattr(struct
, prop
.identifier
)
971 if str(type(val
)) in ["<class 'bpy_prop_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
]
980 dict[prop
.identifier
] = val
982 # io_filters = sv.settings.io_filters
983 dump
["data"] = {"point_of_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])
1001 filepath
= bpy
.path
.ensure_ext(filepath
, '.blsv')
1002 file = gzip
.open(filepath
, mode
='wb')
1003 pickle
.dump(dump
, file, protocol
=pickle
.HIGHEST_PROTOCOL
)
1007 def stored_views_apply_preset(filepath
, replace
=True):
1011 file = gzip
.open(filepath
, mode
='rb')
1012 dump
= pickle
.load(file)
1015 scene
= bpy
.context
.scene
1016 sv
= getattr(scene
, "stored_views", None)
1021 # io_filters = sv.settings.io_filters
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:
1034 sv_list
= sv_data
[sv_struct
] # .list
1035 if replace
is True: # clear swap and list
1036 while len(sv_list
) > 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
]
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
]
1056 setattr(sv_item
, subprop
, subval
)
1058 DataStore
.sanitize_data(scene
)
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(
1073 replace
: BoolProperty(
1076 description
="Replace current stored views, otherwise append"
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
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
)
1103 shutil
.copyfile(self
.filepath
,
1104 os
.path
.join(IO_Utils
.get_preset_path()[0], filename
))
1106 self
.report({'WARNING'},
1107 "Stored Views: preset applied, but installing failed (preset already exists?)")
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(
1120 description
="A current blend scene",
1123 replace
: BoolProperty(
1126 description
="Replace current stored views, otherwise append"
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")
1149 is_finished
= IO_Utils
.stored_views_apply_from_scene(
1150 self
.scene_name
, replace
=self
.replace
1153 self
.report({"WARNING"},
1154 "Could not find the specified scene. Operation Cancelled")
1155 return {"CANCELLED"}
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(
1173 preset_name
: StringProperty(
1176 description
="Name of the stored views preset"
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
)
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")
1203 stored_views
= context
.scene
.stored_views
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')
1219 row
.operator("stored_views.save").index
= -1
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
1235 mode
= stored_views
.mode
1236 for i
in range(len(list)):
1238 icon_string
= "MESH_CUBE" # default icon
1239 # TODO: icons for view
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'
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'
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'
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')
1300 layout
.label(text
="No cameras in this scene")
1303 class VIEW3D_OT_stored_views_preferences(AddonPreferences
):
1304 bl_idname
= __name__
1306 show_exporters
: BoolProperty(
1307 name
="Enable I/O Operators",
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",
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)
1332 VIEW3D_OT_stored_views_initialize
,
1333 VIEW3D_OT_stored_views_preferences
,
1334 VIEW3D_PT_properties_stored_views
,
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
,
1349 VIEW3D_OT_stored_views_import
,
1350 VIEW3D_OT_stored_views_import_from_scene
,
1351 VIEW3D_OT_stored_views_export
1356 from bpy
.utils
import register_class
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__":