diff options
author | meta-androcto <meta.androcto1@gmail.com> | 2019-06-15 03:59:45 +0300 |
---|---|---|
committer | meta-androcto <meta.androcto1@gmail.com> | 2019-06-15 03:59:45 +0300 |
commit | 20a8e9d3f58540cc85da396ca98ee0831903dd23 (patch) | |
tree | ef44bd591e72cba01cce178bedab31e09b4e3345 /space_view3d_stored_views | |
parent | 15b00e9396dfdf955fa1122d2464d5e96f2810b6 (diff) |
space_view3d_stored_views: Restore release status: T63750 d9ffde707fe5
Diffstat (limited to 'space_view3d_stored_views')
-rw-r--r-- | space_view3d_stored_views/__init__.py | 149 | ||||
-rw-r--r-- | space_view3d_stored_views/core.py | 369 | ||||
-rw-r--r-- | space_view3d_stored_views/io.py | 343 | ||||
-rw-r--r-- | space_view3d_stored_views/operators.py | 182 | ||||
-rw-r--r-- | space_view3d_stored_views/properties.py | 137 | ||||
-rw-r--r-- | space_view3d_stored_views/stored_views_test.py | 1371 | ||||
-rw-r--r-- | space_view3d_stored_views/ui.py | 277 |
7 files changed, 2828 insertions, 0 deletions
diff --git a/space_view3d_stored_views/__init__.py b/space_view3d_stored_views/__init__.py new file mode 100644 index 00000000..b438cdbf --- /dev/null +++ b/space_view3d_stored_views/__init__.py @@ -0,0 +1,149 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Stored Views", + "description": "Save and restore User defined views, pov, layers and display configs", + "author": "nfloyd, Francesco Siddi", + "version": (0, 3, 7), + "blender": (2, 80, 0), + "location": "View3D > Properties > Stored Views", + "warning": "", + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.5/" + "Py/Scripts/3D_interaction/stored_views", + "category": "3D View" +} + +""" +ACKNOWLEDGMENT +============== +import/export functionality is mostly based +on Bart Crouch's Theme Manager Addon + +TODO: quadview complete support : investigate. Where's the data? +TODO: lock_camera_and_layers. investigate usage +TODO: list reordering + +NOTE: logging setup has to be provided by the user in a separate config file + as Blender will not try to configure logging by default in an add-on + The Config File should be in the Blender Config folder > /scripts/startup/config_logging.py + For setting up /location of the config folder see: + https://docs.blender.org/manual/en/dev/getting_started/ + installing/configuration/directories.html + For configuring logging itself in the file, general Python documentation should work + As the logging calls are not configured, they can be kept in the other modules of this add-on + and will not have output until the logging configuration is set up +""" + +# if "bpy" in locals(): +# import importlib +# importlib.reload(core) +# importlib.reload(ui) +# importlib.reload(properties) +# importlib.reload(operators) +# importlib.reload(io) +# else: +import bpy +from . import core +from . import ui +from . import properties +from . import operators +from . import io + + +from bpy.props import ( + BoolProperty, + IntProperty, + PointerProperty, +) +from bpy.types import ( + AddonPreferences, + Operator, +) + + +class VIEW3D_stored_views_initialize(Operator): + bl_idname = "view3d.stored_views_initialize" + bl_label = "Initialize" + + @classmethod + def poll(cls, context): + return not hasattr(bpy.types.Scene, 'stored_views') + + def execute(self, context): + bpy.types.Scene.stored_views = PointerProperty( + type=properties.StoredViewsData + ) + scenes = bpy.data.scenes + for scene in scenes: + core.DataStore.sanitize_data(scene) + return {'FINISHED'} + + +# Addon Preferences + +class VIEW3D_stored_views_preferences(AddonPreferences): + bl_idname = __name__ + + show_exporters : BoolProperty( + name="Enable I/O Operators", + default=False, + description="Enable Import/Export Operations in the UI:\n" + "Import Stored Views preset,\n" + "Export Stored Views preset and \n" + "Import stored views from scene", + ) + view_3d_update_rate : IntProperty( + name="3D view update", + description="Update rate of the 3D view redraw\n" + "Increse the value if the UI feels sluggish", + min=1, max=10, + default=1 + ) + + def draw(self, context): + layout = self.layout + + row = layout.row(align=True) + row.prop(self, "view_3d_update_rate", toggle=True) + row.prop(self, "show_exporters", toggle=True) + + +def register(): + ui.register() + properties.register() + operators.register() + io.register() + bpy.utils.register_class(VIEW3D_stored_views_initialize) + bpy.utils.register_class(VIEW3D_stored_views_preferences) + + +def unregister(): + ui.unregister() + properties.unregister() + operators.unregister() + io.unregister() + bpy.utils.unregister_class(VIEW3D_stored_views_initialize) + bpy.utils.unregister_class(VIEW3D_stored_views_preferences) + ui.VIEW3D_stored_views_draw.handle_remove(bpy.context) + if hasattr(bpy.types.Scene, "stored_views"): + del bpy.types.Scene.stored_views + + +if __name__ == "__main__": + register() diff --git a/space_view3d_stored_views/core.py b/space_view3d_stored_views/core.py new file mode 100644 index 00000000..5360b4bb --- /dev/null +++ b/space_view3d_stored_views/core.py @@ -0,0 +1,369 @@ +# gpl authors: nfloyd, Francesco Siddi + + +import logging +module_logger = logging.getLogger(__name__) + +import hashlib +import bpy + + +#Utility function get preferences setting for exporters +def get_preferences(): + # replace the key if the add-on name changes + addon = bpy.context.preferences.addons[__package__] + show_warn = (addon.preferences.show_exporters if addon else False) + + return show_warn + + +class StoredView(): + def __init__(self, mode, index=None): + self.logger = logging.getLogger('%s.StoredView' % __name__) + self.scene = bpy.context.scene + self.view3d = bpy.context.space_data + self.index = index + self.data_store = DataStore(mode=mode) + + def save(self): + if self.index == -1: + stored_view, self.index = self.data_store.create() + else: + stored_view = self.data_store.get(self.index) + self.from_v3d(stored_view) + self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name)) + + def set(self): + stored_view = self.data_store.get(self.index) + self.update_v3d(stored_view) + self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name)) + + def from_v3d(self, stored_view): + raise NotImplementedError("Subclass must implement abstract method") + + def update_v3d(self, stored_view): + raise NotImplementedError("Subclass must implement abstract method") + + @staticmethod + def is_modified(context, stored_view): + raise NotImplementedError("Subclass must implement abstract method") + + +class POV(StoredView): + def __init__(self, index=None): + super().__init__(mode='POV', index=index) + self.logger = logging.getLogger('%s.POV' % __name__) + + def from_v3d(self, stored_view): + view3d = self.view3d + region3d = view3d.region_3d + + stored_view.distance = region3d.view_distance + stored_view.location = region3d.view_location + stored_view.rotation = region3d.view_rotation + stored_view.perspective_matrix_md5 = POV._get_perspective_matrix_md5(region3d) + stored_view.perspective = region3d.view_perspective + stored_view.lens = view3d.lens + stored_view.clip_start = view3d.clip_start + stored_view.clip_end = view3d.clip_end + + if region3d.view_perspective == 'CAMERA': + stored_view.camera_type = view3d.camera.type # type : 'CAMERA' or 'MESH' + stored_view.camera_name = view3d.camera.name # store string instead of object + if view3d.lock_object is not None: + stored_view.lock_object_name = view3d.lock_object.name # idem + else: + stored_view.lock_object_name = "" + stored_view.lock_cursor = view3d.lock_cursor + stored_view.cursor_location = bpy.context.scene.cursor.location + + def update_v3d(self, stored_view): + view3d = self.view3d + region3d = view3d.region_3d + region3d.view_distance = stored_view.distance + region3d.view_location = stored_view.location + region3d.view_rotation = stored_view.rotation + region3d.view_perspective = stored_view.perspective + view3d.lens = stored_view.lens + view3d.clip_start = stored_view.clip_start + view3d.clip_end = stored_view.clip_end + view3d.lock_cursor = stored_view.lock_cursor + if stored_view.lock_cursor is True: + # update cursor only if view is locked to cursor + view3d.cursor_location = stored_view.cursor_location + + if stored_view.perspective == "CAMERA": + + lock_obj = self._get_object(stored_view.lock_object_name) + if lock_obj: + view3d.lock_object = lock_obj + else: + cam = self._get_object(stored_view.camera_name) + if cam: + view3d.camera = cam + + @staticmethod + def _get_object(name, pointer=None): + return bpy.data.objects.get(name) + + @staticmethod + def is_modified(context, stored_view): + # TODO: check for others param, currently only perspective + # and perspective_matrix are checked + POV.logger = logging.getLogger('%s.POV' % __name__) + view3d = context.space_data + region3d = view3d.region_3d + if region3d.view_perspective != stored_view.perspective: + POV.logger.debug('view_perspective') + return True + + md5 = POV._get_perspective_matrix_md5(region3d) + if (md5 != stored_view.perspective_matrix_md5 and + region3d.view_perspective != "CAMERA"): + POV.logger.debug('perspective_matrix') + return True + + return False + + @staticmethod + def _get_perspective_matrix_md5(region3d): + md5 = hashlib.md5(str(region3d.perspective_matrix).encode('utf-8')).hexdigest() + return md5 + + +# class Layers(StoredView): +# def __init__(self, index=None): +# super().__init__(mode='COLLECTIONS', index=index) +# self.logger = logging.getLogger('%s.Layers' % __name__) + +# def from_v3d(self, stored_view): +# view3d = self.view3d +# stored_view.view_layers = bpy.types.layer.collection +# stored_view.scene_layers = self.scene.layers +# stored_view.lock_camera = view3d.lock_camera + +# def update_v3d(self, stored_view): +# view3d = self.view3d +# view3d.camera = stored_view.camera +# if stored_view.camera is True: +# self.scene.layers = stored_view.scene_layers +# else: +# view3d.layers = stored_view.view_layers + + # @staticmethod + # def is_modified(context, stored_view): + # Layers.logger = logging.getLogger('%s.Layers' % __name__) + # if stored_view.camera != context.space_data.camera: + # Layers.logger.debug('lock_camera') + # return True + # if stored_view.camera is True: + # for i in range(20): + # if stored_view.scene_layers[i] != context.scene.layers[i]: + # Layers.logger.debug('scene_layers[%s]' % (i, )) + # return True + # else: + # for i in range(20): + # if stored_view.view_layers[i] != context.space_data.view3d.layers[i]: + # return True + # return False + + +# class Display(StoredView): +# def __init__(self, index=None): +# super().__init__(mode='DISPLAY', index=index) +# self.logger = logging.getLogger('%s.Display' % __name__) + +# def from_v3d(self, stored_view): +# view3d = self.view3d +# stored_view.viewport_shade = view3d.viewport_shade +# stored_view.show_only_render = view3d.show_only_render +# stored_view.show_outline_selected = view3d.show_outline_selected +# stored_view.show_all_objects_origin = view3d.show_all_objects_origin +# stored_view.show_relationship_lines = view3d.show_relationship_lines +# stored_view.show_floor = view3d.show_floor +# stored_view.show_axis_x = view3d.show_axis_x +# stored_view.show_axis_y = view3d.show_axis_y +# stored_view.show_axis_z = view3d.show_axis_z +# stored_view.grid_lines = view3d.grid_lines +# stored_view.grid_scale = view3d.grid_scale +# stored_view.grid_subdivisions = view3d.grid_subdivisions +# stored_view.material_mode = self.scene.game_settings.material_mode +# stored_view.show_textured_solid = view3d.show_textured_solid + + # def update_v3d(self, stored_view): + # view3d = self.view3d + # view3d.viewport_shade = stored_view.viewport_shade + # view3d.show_only_render = stored_view.show_only_render + # view3d.show_outline_selected = stored_view.show_outline_selected + # view3d.show_all_objects_origin = stored_view.show_all_objects_origin + # view3d.show_relationship_lines = stored_view.show_relationship_lines + # view3d.show_floor = stored_view.show_floor + # view3d.show_axis_x = stored_view.show_axis_x + # view3d.show_axis_y = stored_view.show_axis_y + # view3d.show_axis_z = stored_view.show_axis_z + # view3d.grid_lines = stored_view.grid_lines + # view3d.grid_scale = stored_view.grid_scale + # view3d.grid_subdivisions = stored_view.grid_subdivisions + # self.scene.game_settings.material_mode = stored_view.material_mode + # view3d.show_textured_solid = stored_view.show_textured_solid + + # @staticmethod + # def is_modified(context, stored_view): + # Display.logger = logging.getLogger('%s.Display' % __name__) + # view3d = context.space_data + # excludes = ["material_mode", "quad_view", "lock_rotation", "show_sync_view", "use_box_clip", "name"] + # for k, v in stored_view.items(): + # if k not in excludes: + # if getattr(view3d, k) != getattr(stored_view, k): + # return True + + # if stored_view.material_mode != context.scene.game_settings.material_mode: + # Display.logger.debug('material_mode') + # return True + + +class View(StoredView): + def __init__(self, index=None): + super().__init__(mode='VIEW', index=index) + self.logger = logging.getLogger('%s.View' % __name__) + self.pov = POV() + # self.layers = Layers() + # self.display = Display() + + def from_v3d(self, stored_view): + self.pov.from_v3d(stored_view.pov) + # self.layers.from_v3d(stored_view.layers) + # self.display.from_v3d(stored_view.display) + + def update_v3d(self, stored_view): + self.pov.update_v3d(stored_view.pov) + # self.layers.update_v3d(stored_view.layers) + # self.display.update_v3d(stored_view.display) + + @staticmethod + def is_modified(context, stored_view): + POV.is_modified(context, stored_view.pov) #or \ + # Layers.is_modified(context, stored_view.layers) or \ + # Display.is_modified(context, stored_view.display): + return True + # return False + + +class DataStore(): + def __init__(self, scene=None, mode=None): + if scene is None: + scene = bpy.context.scene + stored_views = scene.stored_views + self.mode = mode + + if mode is None: + self.mode = stored_views.mode + + if self.mode == 'VIEW': + self.list = stored_views.view_list + self.current_index = stored_views.current_indices[0] + elif self.mode == 'POV': + self.list = stored_views.pov_list + self.current_index = stored_views.current_indices[1] + elif self.mode == 'LAYERS': + self.list = stored_views.layers_list + self.current_index = stored_views.current_indices[2] + elif self.mode == 'DISPLAY': + self.list = stored_views.display_list + self.current_index = stored_views.current_indices[3] + + def create(self): + item = self.list.add() + item.name = self._generate_name() + index = len(self.list) - 1 + self._set_current_index(index) + return item, index + + def get(self, index): + self._set_current_index(index) + return self.list[index] + + def delete(self, index): + if self.current_index > index: + self._set_current_index(self.current_index - 1) + elif self.current_index == index: + self._set_current_index(-1) + + self.list.remove(index) + + def _set_current_index(self, index): + self.current_index = index + mode = self.mode + stored_views = bpy.context.scene.stored_views + if mode == 'VIEW': + stored_views.current_indices[0] = index + elif mode == 'POV': + stored_views.current_indices[1] = index + elif mode == 'LAYERS': + stored_views.current_indices[2] = index + elif mode == 'DISPLAY': + stored_views.current_indices[3] = index + + def _generate_name(self): + default_name = str(self.mode) + names = [] + for i in self.list: + i_name = i.name + if i_name.startswith(default_name): + names.append(i_name) + names.sort() + try: + l_name = names[-1] + post_fix = l_name.rpartition('.')[2] + if post_fix.isnumeric(): + post_fix = str(int(post_fix) + 1).zfill(3) + else: + if post_fix == default_name: + post_fix = "001" + return default_name + "." + post_fix + except: + return default_name + + @staticmethod + def sanitize_data(scene): + + def check_objects_references(mode, list): + to_remove = [] + for i, list_item in enumerate(list.items()): + key, item = list_item + if mode == 'POV' or mode == 'VIEWS': + if mode == 'VIEWS': + item = item.pov + + if item.perspective == "CAMERA": + + camera = bpy.data.objects.get(item.camera_name) + if camera is None: + try: # pick a default camera TODO: ask to pick? + camera = bpy.data.cameras[0] + item.camera_name = camera.name + except: # couldn't find a camera in the scene + pass + + obj = bpy.data.objects.get(item.lock_object_name) + if obj is None and camera is None: + to_remove.append(i) + + for i in reversed(to_remove): + list.remove(i) + + modes = ['POV', 'VIEW', 'DISPLAY', 'LAYERS'] + for mode in modes: + data = DataStore(scene=scene, mode=mode) + check_objects_references(mode, data.list) + + +def stored_view_factory(mode, *args, **kwargs): + if mode == 'POV': + return POV(*args, **kwargs) + elif mode == 'LAYERS': + return Layers(*args, **kwargs) + elif mode == 'DISPLAY': + return Display(*args, **kwargs) + elif mode == 'VIEW': + return View(*args, **kwargs) diff --git a/space_view3d_stored_views/io.py b/space_view3d_stored_views/io.py new file mode 100644 index 00000000..72237ed3 --- /dev/null +++ b/space_view3d_stored_views/io.py @@ -0,0 +1,343 @@ +# gpl authors: nfloyd, Francesco Siddi + +import gzip +import os +import pickle +import shutil + +import bpy +from bpy.types import Operator +from bpy.props import ( + BoolProperty, + StringProperty, +) +from bpy_extras.io_utils import ( + ExportHelper, + ImportHelper, +) +from .import bl_info +from .core import get_preferences +from .operators import DataStore + + +# TODO: reinstate filters? +class IO_Utils(): + + @staticmethod + def get_preset_path(): + # locate stored_views preset folder + paths = bpy.utils.preset_paths("stored_views") + if not paths: + # stored_views preset folder doesn't exist, so create it + paths = [os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets", + "stored_views")] + if not os.path.exists(paths[0]): + os.makedirs(paths[0]) + + return(paths) + + @staticmethod + def stored_views_apply_from_scene(scene_name, replace=True): + scene = bpy.context.scene + scene_exists = True if scene_name in bpy.data.scenes.keys() else False + + if scene_exists: + sv = bpy.context.scene.stored_views + # io_filters = sv.settings.io_filters + + structs = [sv.view_list, sv.pov_list, sv.layers_list, sv.display_list] + if replace is True: + for st in structs: # clear swap and list + while len(st) > 0: + st.remove(0) + + f_sv = bpy.data.scenes[scene_name].stored_views + # f_sv = bpy.data.scenes[scene_name].stored_views + f_structs = [f_sv.view_list, f_sv.pov_list, f_sv.layers_list, f_sv.display_list] + """ + is_filtered = [io_filters.views, io_filters.point_of_views, + io_filters.layers, io_filters.displays] + """ + for i in range(len(f_structs)): + """ + if is_filtered[i] is False: + continue + """ + for j in f_structs[i]: + item = structs[i].add() + # stored_views_copy_item(j, item) + for k, v in j.items(): + item[k] = v + DataStore.sanitize_data(scene) + return True + else: + return False + + @staticmethod + def stored_views_export_to_blsv(filepath, name='Custom Preset'): + # create dictionary with all information + dump = {"info": {}, "data": {}} + dump["info"]["script"] = bl_info['name'] + dump["info"]["script_version"] = bl_info['version'] + dump["info"]["version"] = bpy.app.version + dump["info"]["preset_name"] = name + + # get current stored views settings + scene = bpy.context.scene + sv = scene.stored_views + + def dump_view_list(dict, list): + if str(type(list)) == "<class 'bpy_prop_collection_idprop'>": + for i, struct_dict in enumerate(list): + dict[i] = {"name": str, + "pov": {}, + "layers": {}, + "display": {}} + dict[i]["name"] = struct_dict.name + dump_item(dict[i]["pov"], struct_dict.pov) + dump_item(dict[i]["layers"], struct_dict.layers) + dump_item(dict[i]["display"], struct_dict.display) + + def dump_list(dict, list): + if str(type(list)) == "<class 'bpy_prop_collection_idprop'>": + for i, struct in enumerate(list): + dict[i] = {} + dump_item(dict[i], struct) + + def dump_item(dict, struct): + for prop in struct.bl_rna.properties: + if prop.identifier == "rna_type": + # not a setting, so skip + continue + + val = getattr(struct, prop.identifier) + if str(type(val)) in ["<class 'bpy_prop_array'>"]: + # array + dict[prop.identifier] = [v for v in val] + # address the pickle limitations of dealing with the Vector class + elif str(type(val)) in ["<class 'Vector'>", + "<class 'Quaternion'>"]: + dict[prop.identifier] = [v for v in val] + else: + # single value + dict[prop.identifier] = val + + # io_filters = sv.settings.io_filters + dump["data"] = {"point_of_views": {}, + "layers": {}, + "displays": {}, + "views": {}} + + others_data = [(dump["data"]["point_of_views"], sv.pov_list), # , io_filters.point_of_views), + (dump["data"]["layers"], sv.layers_list), # , io_filters.layers), + (dump["data"]["displays"], sv.display_list)] # , io_filters.displays)] + for list_data in others_data: + # if list_data[2] is True: + dump_list(list_data[0], list_data[1]) + + views_data = (dump["data"]["views"], sv.view_list) + # if io_filters.views is True: + dump_view_list(views_data[0], views_data[1]) + + # save to file + filepath = filepath + filepath = bpy.path.ensure_ext(filepath, '.blsv') + file = gzip.open(filepath, mode='wb') + pickle.dump(dump, file, protocol=pickle.HIGHEST_PROTOCOL) + file.close() + + @staticmethod + def stored_views_apply_preset(filepath, replace=True): + if not filepath: + return False + + file = gzip.open(filepath, mode='rb') + dump = pickle.load(file) + file.close() + # apply preset + scene = bpy.context.scene + sv = getattr(scene, "stored_views", None) + + if not sv: + return False + + # io_filters = sv.settings.io_filters + sv_data = { + "point_of_views": sv.pov_list, + "views": sv.view_list, + "layers": sv.layers_list, + "displays": sv.display_list + } + for sv_struct, props in dump["data"].items(): + """ + is_filtered = getattr(io_filters, sv_struct) + if is_filtered is False: + continue + """ + sv_list = sv_data[sv_struct] # .list + if replace is True: # clear swap and list + while len(sv_list) > 0: + sv_list.remove(0) + for key, prop_struct in props.items(): + sv_item = sv_list.add() + + for subprop, subval in prop_struct.items(): + if isinstance(subval, dict): # views : pov, layers, displays + v_subprop = getattr(sv_item, subprop) + for v_subkey, v_subval in subval.items(): + if isinstance(v_subval, list): # array like of pov,... + v_array_like = getattr(v_subprop, v_subkey) + for i in range(len(v_array_like)): + v_array_like[i] = v_subval[i] + else: + setattr(v_subprop, v_subkey, v_subval) # others + elif isinstance(subval, list): + array_like = getattr(sv_item, subprop) + for i in range(len(array_like)): + array_like[i] = subval[i] + else: + setattr(sv_item, subprop, subval) + + DataStore.sanitize_data(scene) + + return True + + +class VIEW3D_stored_views_import(Operator, ImportHelper): + bl_idname = "stored_views.import_blsv" + bl_label = "Import Stored Views preset" + bl_description = "Import a .blsv preset file to the current Stored Views" + + filename_ext = ".blsv" + filter_glob = StringProperty( + default="*.blsv", + options={'HIDDEN'} + ) + replace = BoolProperty( + name="Replace", + default=True, + description="Replace current stored views, otherwise append" + ) + + @classmethod + def poll(cls, context): + return get_preferences() + + def execute(self, context): + # the usual way is to not select the file in the file browser + exists = os.path.isfile(self.filepath) if self.filepath else False + if not exists: + self.report({'WARNING'}, + "No filepath specified or file could not be found. Operation Cancelled") + return {'CANCELLED'} + + # apply chosen preset + apply_preset = IO_Utils.stored_views_apply_preset( + filepath=self.filepath, replace=self.replace + ) + if not apply_preset: + self.report({'WARNING'}, + "Please Initialize Stored Views first (in the 3D View Properties Area)") + return {'CANCELLED'} + + # copy preset to presets folder + filename = os.path.basename(self.filepath) + try: + shutil.copyfile(self.filepath, + os.path.join(IO_Utils.get_preset_path()[0], filename)) + except: + self.report({'WARNING'}, + "Stored Views: preset applied, but installing failed (preset already exists?)") + return{'CANCELLED'} + + return{'FINISHED'} + + +class VIEW3D_stored_views_import_from_scene(Operator): + bl_idname = "stored_views.import_from_scene" + bl_label = "Import stored views from scene" + bl_description = "Import currently stored views from an another scene" + + scene_name = StringProperty( + name="Scene Name", + description="A current blend scene", + default="" + ) + replace = BoolProperty( + name="Replace", + default=True, + description="Replace current stored views, otherwise append" + ) + + @classmethod + def poll(cls, context): + return get_preferences() + + def draw(self, context): + layout = self.layout + + layout.prop_search(self, "scene_name", bpy.data, "scenes") + layout.prop(self, "replace") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + # filepath should always be given + if not self.scene_name: + self.report({"WARNING"}, + "No scene name was given. Operation Cancelled") + return{'CANCELLED'} + + is_finished = IO_Utils.stored_views_apply_from_scene( + self.scene_name, replace=self.replace + ) + if not is_finished: + self.report({"WARNING"}, + "Could not find the specified scene. Operation Cancelled") + return {"CANCELLED"} + + return{'FINISHED'} + + +class VIEW3D_stored_views_export(Operator, ExportHelper): + bl_idname = "stored_views.export_blsv" + bl_label = "Export Stored Views preset" + bl_description = "Export the current Stored Views to a .blsv preset file" + + filename_ext = ".blsv" + filepath = StringProperty( + default=os.path.join(IO_Utils.get_preset_path()[0], "untitled") + ) + filter_glob = StringProperty( + default="*.blsv", + options={'HIDDEN'} + ) + preset_name = StringProperty( + name="Preset name", + default="", + description="Name of the stored views preset" + ) + + @classmethod + def poll(cls, context): + return get_preferences() + + def execute(self, context): + IO_Utils.stored_views_export_to_blsv(self.filepath, self.preset_name) + + return{'FINISHED'} + +classes = ( + VIEW3D_stored_views_import, + VIEW3D_stored_views_import_from_scene, + VIEW3D_stored_views_export +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls)
\ No newline at end of file diff --git a/space_view3d_stored_views/operators.py b/space_view3d_stored_views/operators.py new file mode 100644 index 00000000..4cf8a2cf --- /dev/null +++ b/space_view3d_stored_views/operators.py @@ -0,0 +1,182 @@ +# gpl authors: nfloyd, Francesco Siddi + +import bpy +from bpy.props import IntProperty +from bpy.types import Operator +from .core import ( + stored_view_factory, + DataStore, + ) +from .ui import init_draw + + +class VIEW3D_stored_views_save(Operator): + bl_idname = "stored_views.save" + bl_label = "Save Current" + bl_description = "Save the view 3d current state" + + index : IntProperty() + + def execute(self, context): + mode = context.scene.stored_views.mode + sv = stored_view_factory(mode, self.index) + sv.save() + context.scene.stored_views.view_modified = False + init_draw(context) + + return {'FINISHED'} + + +class VIEW3D_stored_views_set(Operator): + bl_idname = "stored_views.set" + bl_label = "Set" + bl_description = "Update the view 3D according to this view" + + index : IntProperty() + + def execute(self, context): + mode = context.scene.stored_views.mode + sv = stored_view_factory(mode, self.index) + sv.set() + context.scene.stored_views.view_modified = False + init_draw(context) + + return {'FINISHED'} + + +class VIEW3D_stored_views_delete(Operator): + bl_idname = "stored_views.delete" + bl_label = "Delete" + bl_description = "Delete this view" + + index : IntProperty() + + def execute(self, context): + data = DataStore() + data.delete(self.index) + + return {'FINISHED'} + + +class VIEW3D_New_Camera_to_View(Operator): + bl_idname = "stored_views.newcamera" + bl_label = "New Camera To View" + bl_description = "Add a new Active Camera and align it to this view" + + @classmethod + def poll(cls, context): + return ( + context.space_data is not None and + context.space_data.type == 'VIEW_3D' and + context.space_data.region_3d.view_perspective != 'CAMERA' + ) + + def execute(self, context): + + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + + bpy.ops.object.camera_add() + cam = context.active_object + cam.name = "View_Camera" + # make active camera by hand + context.scene.camera = cam + + bpy.ops.view3d.camera_to_view() + return {'FINISHED'} + + +# Camera marker & switcher by Fsiddi +class SetSceneCamera(Operator): + bl_idname = "cameraselector.set_scene_camera" + bl_label = "Set Scene Camera" + bl_description = "Set chosen camera as the scene's active camera" + + hide_others = False + + def execute(self, context): + chosen_camera = context.active_object + scene = context.scene + + if self.hide_others: + for c in [o for o in scene.objects if o.type == 'CAMERA']: + c.hide = (c != chosen_camera) + scene.camera = chosen_camera + bpy.ops.object.select_all(action ='DESELECT') + chosen_camera.select_set(True) + return {'FINISHED'} + + def invoke(self, context, event): + if event.ctrl: + self.hide_others = True + + return self.execute(context) + + +class PreviewSceneCamera(Operator): + bl_idname = "cameraselector.preview_scene_camera" + bl_label = "Preview Camera" + bl_description = "Preview chosen camera and make scene's active camera" + + def execute(self, context): + chosen_camera = context.active_object + bpy.ops.view3d.object_as_camera() + bpy.ops.object.select_all(action="DESELECT") + chosen_camera.select_set(True) + return {'FINISHED'} + + +class AddCameraMarker(Operator): + bl_idname = "cameraselector.add_camera_marker" + bl_label = "Add Camera Marker" + bl_description = "Add a timeline marker bound to chosen camera" + + def execute(self, context): + chosen_camera = context.active_object + scene = context.scene + + current_frame = scene.frame_current + marker = None + for m in reversed(sorted(filter(lambda m: m.frame <= current_frame, + scene.timeline_markers), + key=lambda m: m.frame)): + marker = m + break + if marker and (marker.camera == chosen_camera): + # Cancel if the last marker at or immediately before + # current frame is already bound to the camera. + return {'CANCELLED'} + + marker_name = "F_%02d_%s" % (current_frame, chosen_camera.name) + if marker and (marker.frame == current_frame): + # Reuse existing marker at current frame to avoid + # overlapping bound markers. + marker.name = marker_name + else: + marker = scene.timeline_markers.new(marker_name) + marker.frame = scene.frame_current + marker.camera = chosen_camera + marker.select = True + + for other_marker in [m for m in scene.timeline_markers if m != marker]: + other_marker.select = True + + return {'FINISHED'} + +classes = ( + VIEW3D_stored_views_save, + VIEW3D_stored_views_set, + VIEW3D_stored_views_delete, + VIEW3D_New_Camera_to_View, + SetSceneCamera, + PreviewSceneCamera, + AddCameraMarker +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls)
\ No newline at end of file diff --git a/space_view3d_stored_views/properties.py b/space_view3d_stored_views/properties.py new file mode 100644 index 00000000..0bf95097 --- /dev/null +++ b/space_view3d_stored_views/properties.py @@ -0,0 +1,137 @@ +# gpl authors: nfloyd, Francesco Siddi +import bpy +from bpy.types import PropertyGroup +from bpy.props import ( + BoolProperty, + BoolVectorProperty, + CollectionProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + IntProperty, + IntVectorProperty, + PointerProperty, + StringProperty, + ) + + +class POVData(PropertyGroup): + distance : FloatProperty() + location : FloatVectorProperty( + subtype='TRANSLATION' + ) + rotation : FloatVectorProperty( + subtype='QUATERNION', + size=4 + ) + name : StringProperty() + perspective : EnumProperty( + items=[('PERSP', '', ''), + ('ORTHO', '', ''), + ('CAMERA', '', '')] + ) + lens : FloatProperty() + clip_start : FloatProperty() + clip_end : FloatProperty() + lock_cursor : BoolProperty() + cursor_location : FloatVectorProperty() + perspective_matrix_md5 = StringProperty() + camera_name : StringProperty() + camera_type : StringProperty() + lock_object_name : StringProperty() + + +class LayersData(PropertyGroup): + view_layers : BoolVectorProperty(size=20) + scene_layers : BoolVectorProperty(size=20) + lock_camera_and_layers : BoolProperty() + name : StringProperty() + + +class DisplayData(PropertyGroup): + name = StringProperty() + viewport_shade = EnumProperty( + items=[('BOUNDBOX', 'BOUNDBOX', 'BOUNDBOX'), + ('WIREFRAME', 'WIREFRAME', 'WIREFRAME'), + ('SOLID', 'SOLID', 'SOLID'), + ('TEXTURED', 'TEXTURED', 'TEXTURED'), + ('MATERIAL', 'MATERIAL', 'MATERIAL'), + ('RENDERED', 'RENDERED', 'RENDERED')] + ) + show_only_render : BoolProperty() + show_outline_selected : BoolProperty() + show_all_objects_origin : BoolProperty() + show_relationship_lines : BoolProperty() + show_floor : BoolProperty() + show_axis_x : BoolProperty() + show_axis_y : BoolProperty() + show_axis_z : BoolProperty() + grid_lines : IntProperty() + grid_scale : FloatProperty() + grid_subdivisions : IntProperty() + material_mode : StringProperty() + show_textured_solid : BoolProperty() + quad_view : BoolProperty() + lock_rotation : BoolProperty() + show_sync_view : BoolProperty() + use_box_clip : BoolProperty() + + +class ViewData(PropertyGroup): + pov : PointerProperty( + type=POVData + ) + layers : PointerProperty( + type=LayersData + ) + display : PointerProperty( + type=DisplayData + ) + name : StringProperty() + + +class StoredViewsData(PropertyGroup): + pov_list : CollectionProperty( + type=POVData + ) + layers_list : CollectionProperty( + type=LayersData + ) + display_list : CollectionProperty( + type=DisplayData + ) + view_list : CollectionProperty( + type=ViewData + ) + mode : EnumProperty( + name="Mode", + items=[('VIEW', "View", "3D View settings"), + ('POV', "POV", "POV settings"), + ('LAYERS', "Layers", "Layers settings"), + ('DISPLAY', "Display", "Display settings")], + default='VIEW' + ) + current_indices : IntVectorProperty( + size=4, + default=[-1, -1, -1, -1] + ) + view_modified : BoolProperty( + default=False + ) + + +classes = ( + POVData, + LayersData, + DisplayData, + ViewData, + StoredViewsData, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls)
\ No newline at end of file diff --git a/space_view3d_stored_views/stored_views_test.py b/space_view3d_stored_views/stored_views_test.py new file mode 100644 index 00000000..fa4866f0 --- /dev/null +++ b/space_view3d_stored_views/stored_views_test.py @@ -0,0 +1,1371 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Stored Views", + "description": "Save and restore User defined views, pov, layers and display configs", + "author": "nfloyd, Francesco Siddi", + "version": (0, 3, 7), + "blender": (2, 80, 0), + "location": "View3D > Properties > Stored Views", + "warning": "", + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.5/" + "Py/Scripts/3D_interaction/stored_views", + "category": "3D View" +} + +""" +ACKNOWLEDGMENT +============== +import/export functionality is mostly based +on Bart Crouch's Theme Manager Addon + +TODO: quadview complete support : investigate. Where's the data? +TODO: lock_camera_and_layers. investigate usage +TODO: list reordering + +NOTE: logging setup has to be provided by the user in a separate config file + as Blender will not try to configure logging by default in an add-on + The Config File should be in the Blender Config folder > /scripts/startup/config_logging.py + For setting up /location of the config folder see: + https://docs.blender.org/manual/en/dev/getting_started/ + installing/configuration/directories.html + For configuring logging itself in the file, general Python documentation should work + As the logging calls are not configured, they can be kept in the other modules of this add-on + and will not have output until the logging configuration is set up +""" + + +import bpy +from bpy.props import ( + BoolProperty, + IntProperty, + PointerProperty, +) +from bpy.types import ( + AddonPreferences, + Operator, + Panel +) + +import logging +module_logger = logging.getLogger(__name__) + +import gzip +import os +import pickle +import shutil + +from bpy_extras.io_utils import ( + ExportHelper, + ImportHelper, +) + +import blf + +import hashlib +import bpy + + +# Utility function get preferences setting for exporters +def get_preferences(): + # replace the key if the add-on name changes + addon = bpy.context.preferences.addons[__package__] + show_warn = (addon.preferences.show_exporters if addon else False) + + return show_warn + + +class StoredView(): + def __init__(self, mode, index=None): + self.logger = logging.getLogger('%s.StoredView' % __name__) + self.scene = bpy.context.scene + self.view3d = bpy.context.space_data + self.index = index + self.data_store = DataStore(mode=mode) + + def save(self): + if self.index == -1: + stored_view, self.index = self.data_store.create() + else: + stored_view = self.data_store.get(self.index) + self.from_v3d(stored_view) + self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name)) + + def set(self): + stored_view = self.data_store.get(self.index) + self.update_v3d(stored_view) + self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name)) + + def from_v3d(self, stored_view): + raise NotImplementedError("Subclass must implement abstract method") + + def update_v3d(self, stored_view): + raise NotImplementedError("Subclass must implement abstract method") + + @staticmethod + def is_modified(context, stored_view): + raise NotImplementedError("Subclass must implement abstract method") + + +class POV(StoredView): + def __init__(self, index=None): + super().__init__(mode='POV', index=index) + self.logger = logging.getLogger('%s.POV' % __name__) + + def from_v3d(self, stored_view): + view3d = self.view3d + region3d = view3d.region_3d + + stored_view.distance = region3d.view_distance + stored_view.location = region3d.view_location + stored_view.rotation = region3d.view_rotation + stored_view.perspective_matrix_md5 = POV._get_perspective_matrix_md5(region3d) + stored_view.perspective = region3d.view_perspective + stored_view.lens = view3d.lens + stored_view.clip_start = view3d.clip_start + stored_view.clip_end = view3d.clip_end + + if region3d.view_perspective == 'CAMERA': + stored_view.camera_type = view3d.camera.type # type : 'CAMERA' or 'MESH' + stored_view.camera_name = view3d.camera.name # store string instead of object + if view3d.lock_object is not None: + stored_view.lock_object_name = view3d.lock_object.name # idem + else: + stored_view.lock_object_name = "" + stored_view.lock_cursor = view3d.lock_cursor + stored_view.cursor_location = view3d.cursor_location + + def update_v3d(self, stored_view): + view3d = self.view3d + region3d = view3d.region_3d + region3d.view_distance = stored_view.distance + region3d.view_location = stored_view.location + region3d.view_rotation = stored_view.rotation + region3d.view_perspective = stored_view.perspective + view3d.lens = stored_view.lens + view3d.clip_start = stored_view.clip_start + view3d.clip_end = stored_view.clip_end + view3d.lock_cursor = stored_view.lock_cursor + if stored_view.lock_cursor is True: + # update cursor only if view is locked to cursor + view3d.cursor_location = stored_view.cursor_location + + if stored_view.perspective == "CAMERA": + + lock_obj = self._get_object(stored_view.lock_object_name) + if lock_obj: + view3d.lock_object = lock_obj + else: + cam = self._get_object(stored_view.camera_name) + if cam: + view3d.camera = cam + + @staticmethod + def _get_object(name, pointer=None): + return bpy.data.objects.get(name) + + @staticmethod + def is_modified(context, stored_view): + # TODO: check for others param, currently only perspective + # and perspective_matrix are checked + POV.logger = logging.getLogger('%s.POV' % __name__) + view3d = context.space_data + region3d = view3d.region_3d + if region3d.view_perspective != stored_view.perspective: + POV.logger.debug('view_perspective') + return True + + md5 = POV._get_perspective_matrix_md5(region3d) + if (md5 != stored_view.perspective_matrix_md5 and + region3d.view_perspective != "CAMERA"): + POV.logger.debug('perspective_matrix') + return True + + return False + + @staticmethod + def _get_perspective_matrix_md5(region3d): + md5 = hashlib.md5(str(region3d.perspective_matrix).encode('utf-8')).hexdigest() + return md5 + + +class Layers(StoredView): + def __init__(self, index=None): + super().__init__(mode='LAYERS', index=index) + self.logger = logging.getLogger('%s.Layers' % __name__) + + def from_v3d(self, stored_view): + view3d = self.view3d + stored_view.view_layers = view3d.layers + stored_view.scene_layers = self.scene.layers + stored_view.lock_camera_and_layers = view3d.lock_camera_and_layers + + def update_v3d(self, stored_view): + view3d = self.view3d + view3d.lock_camera_and_layers = stored_view.lock_camera_and_layers + if stored_view.lock_camera_and_layers is True: + self.scene.layers = stored_view.scene_layers + else: + view3d.layers = stored_view.view_layers + + @staticmethod + def is_modified(context, stored_view): + Layers.logger = logging.getLogger('%s.Layers' % __name__) + if stored_view.lock_camera_and_layers != context.space_data.lock_camera_and_layers: + Layers.logger.debug('lock_camera_and_layers') + return True + if stored_view.lock_camera_and_layers is True: + for i in range(20): + if stored_view.scene_layers[i] != context.scene.layers[i]: + Layers.logger.debug('scene_layers[%s]' % (i, )) + return True + else: + for i in range(20): + if stored_view.view_layers[i] != context.space_data.view3d.layers[i]: + return True + return False + + +class Display(StoredView): + def __init__(self, index=None): + super().__init__(mode='DISPLAY', index=index) + self.logger = logging.getLogger('%s.Display' % __name__) + + def from_v3d(self, stored_view): + view3d = self.view3d + stored_view.viewport_shade = view3d.viewport_shade + stored_view.show_only_render = view3d.show_only_render + stored_view.show_outline_selected = view3d.show_outline_selected + stored_view.show_all_objects_origin = view3d.show_all_objects_origin + stored_view.show_relationship_lines = view3d.show_relationship_lines + stored_view.show_floor = view3d.show_floor + stored_view.show_axis_x = view3d.show_axis_x + stored_view.show_axis_y = view3d.show_axis_y + stored_view.show_axis_z = view3d.show_axis_z + stored_view.grid_lines = view3d.grid_lines + stored_view.grid_scale = view3d.grid_scale + stored_view.grid_subdivisions = view3d.grid_subdivisions + stored_view.material_mode = self.scene.game_settings.material_mode + stored_view.show_textured_solid = view3d.show_textured_solid + + def update_v3d(self, stored_view): + view3d = self.view3d + view3d.viewport_shade = stored_view.viewport_shade + view3d.show_only_render = stored_view.show_only_render + view3d.show_outline_selected = stored_view.show_outline_selected + view3d.show_all_objects_origin = stored_view.show_all_objects_origin + view3d.show_relationship_lines = stored_view.show_relationship_lines + view3d.show_floor = stored_view.show_floor + view3d.show_axis_x = stored_view.show_axis_x + view3d.show_axis_y = stored_view.show_axis_y + view3d.show_axis_z = stored_view.show_axis_z + view3d.grid_lines = stored_view.grid_lines + view3d.grid_scale = stored_view.grid_scale + view3d.grid_subdivisions = stored_view.grid_subdivisions + self.scene.game_settings.material_mode = stored_view.material_mode + view3d.show_textured_solid = stored_view.show_textured_solid + + @staticmethod + def is_modified(context, stored_view): + Display.logger = logging.getLogger('%s.Display' % __name__) + view3d = context.space_data + excludes = ["material_mode", "quad_view", "lock_rotation", "show_sync_view", "use_box_clip", "name"] + for k, v in stored_view.items(): + if k not in excludes: + if getattr(view3d, k) != getattr(stored_view, k): + return True + + if stored_view.material_mode != context.scene.game_settings.material_mode: + Display.logger.debug('material_mode') + return True + + +class View(StoredView): + def __init__(self, index=None): + super().__init__(mode='VIEW', index=index) + self.logger = logging.getLogger('%s.View' % __name__) + self.pov = POV() + self.layers = Layers() + self.display = Display() + + def from_v3d(self, stored_view): + self.pov.from_v3d(stored_view.pov) + self.layers.from_v3d(stored_view.layers) + self.display.from_v3d(stored_view.display) + + def update_v3d(self, stored_view): + self.pov.update_v3d(stored_view.pov) + self.layers.update_v3d(stored_view.layers) + self.display.update_v3d(stored_view.display) + + @staticmethod + def is_modified(context, stored_view): + if POV.is_modified(context, stored_view.pov) or \ + Layers.is_modified(context, stored_view.layers) or \ + Display.is_modified(context, stored_view.display): + return True + return False + + +class DataStore(): + def __init__(self, scene=None, mode=None): + if scene is None: + scene = bpy.context.scene + stored_views = scene.stored_views + self.mode = mode + + if mode is None: + self.mode = stored_views.mode + + if self.mode == 'VIEW': + self.list = stored_views.view_list + self.current_index = stored_views.current_indices[0] + elif self.mode == 'POV': + self.list = stored_views.pov_list + self.current_index = stored_views.current_indices[1] + elif self.mode == 'LAYERS': + self.list = stored_views.layers_list + self.current_index = stored_views.current_indices[2] + elif self.mode == 'DISPLAY': + self.list = stored_views.display_list + self.current_index = stored_views.current_indices[3] + + def create(self): + item = self.list.add() + item.name = self._generate_name() + index = len(self.list) - 1 + self._set_current_index(index) + return item, index + + def get(self, index): + self._set_current_index(index) + return self.list[index] + + def delete(self, index): + if self.current_index > index: + self._set_current_index(self.current_index - 1) + elif self.current_index == index: + self._set_current_index(-1) + + self.list.remove(index) + + def _set_current_index(self, index): + self.current_index = index + mode = self.mode + stored_views = bpy.context.scene.stored_views + if mode == 'VIEW': + stored_views.current_indices[0] = index + elif mode == 'POV': + stored_views.current_indices[1] = index + elif mode == 'LAYERS': + stored_views.current_indices[2] = index + elif mode == 'DISPLAY': + stored_views.current_indices[3] = index + + def _generate_name(self): + default_name = str(self.mode) + names = [] + for i in self.list: + i_name = i.name + if i_name.startswith(default_name): + names.append(i_name) + names.sort() + try: + l_name = names[-1] + post_fix = l_name.rpartition('.')[2] + if post_fix.isnumeric(): + post_fix = str(int(post_fix) + 1).zfill(3) + else: + if post_fix == default_name: + post_fix = "001" + return default_name + "." + post_fix + except: + return default_name + + @staticmethod + def sanitize_data(scene): + + def check_objects_references(mode, list): + to_remove = [] + for i, list_item in enumerate(list.items()): + key, item = list_item + if mode == 'POV' or mode == 'VIEWS': + if mode == 'VIEWS': + item = item.pov + + if item.perspective == "CAMERA": + + camera = bpy.data.objects.get(item.camera_name) + if camera is None: + try: # pick a default camera TODO: ask to pick? + camera = bpy.data.cameras[0] + item.camera_name = camera.name + except: # couldn't find a camera in the scene + pass + + obj = bpy.data.objects.get(item.lock_object_name) + if obj is None and camera is None: + to_remove.append(i) + + for i in reversed(to_remove): + list.remove(i) + + modes = ['POV', 'VIEW', 'DISPLAY', 'LAYERS'] + for mode in modes: + data = DataStore(scene=scene, mode=mode) + check_objects_references(mode, data.list) + + +def stored_view_factory(mode, *args, **kwargs): + if mode == 'POV': + return POV(*args, **kwargs) + elif mode == 'LAYERS': + return Layers(*args, **kwargs) + elif mode == 'DISPLAY': + return Display(*args, **kwargs) + elif mode == 'VIEW': + return View(*args, **kwargs) + +""" + If view name display is enabled, + it will check periodically if the view has been modified + since last set. + get_preferences_timer() is the time in seconds between these checks. + It can be increased, if the view become sluggish + It is set in the add-on preferences +""" + + +# Utility function get_preferences_timer for update of 3d view draw +def get_preferences_timer(): + # replace the key if the add-on name changes + # TODO: expose refresh rate to ui??? + addon = bpy.context.preferences.addons[__package__] + timer_update = (addon.preferences.view_3d_update_rate if addon else False) + + return timer_update + + +def init_draw(context=None): + if context is None: + context = bpy.context + + if "stored_views_osd" not in context.window_manager: + context.window_manager["stored_views_osd"] = False + + if not context.window_manager["stored_views_osd"]: + context.window_manager["stored_views_osd"] = True + bpy.ops.stored_views.draw() + + +def _draw_callback_px(self, context): + if context.area and context.area.type == 'VIEW_3D': + r_width = text_location = context.region.width + r_height = context.region.height + font_id = 0 # TODO: need to find out how best to get font_id + + blf.size(font_id, 11, context.preferences.system.dpi) + text_size = blf.dimensions(0, self.view_name) + + # compute the text location + text_location = 0 + overlap = context.preferences.system.use_region_overlap + if overlap: + for region in context.area.regions: + if region.type == "UI": + text_location = r_width - region.width + + text_x = text_location - text_size[0] - 10 + text_y = r_height - text_size[1] - 8 + blf.position(font_id, text_x, text_y, 0) + blf.draw(font_id, self.view_name) + + +class VIEW3D_OT_stored_views_draw(Operator): + bl_idname = "stored_views.draw" + bl_label = "Show current" + bl_description = "Toggle the display current view name in the view 3D" + + _handle = None + _timer = None + + @staticmethod + def handle_add(self, context): + VIEW3D_OT_stored_views_draw._handle = bpy.types.SpaceView3D.draw_handler_add( + _draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') + VIEW3D_OT_stored_views_draw._timer = \ + context.window_manager.event_timer_add(get_preferences_timer(), context.window) + + @staticmethod + def handle_remove(context): + if VIEW3D_OT_stored_views_draw._handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(VIEW3D_OT_stored_views_draw._handle, 'WINDOW') + if VIEW3D_OT_stored_views_draw._timer is not None: + context.window_manager.event_timer_remove(VIEW3D_OT_stored_views_draw._timer) + VIEW3D_OT_stored_views_draw._handle = None + VIEW3D_OT_stored_views_draw._timer = None + + @classmethod + def poll(cls, context): + # return context.mode == 'OBJECT' + return True + + def modal(self, context, event): + if context.area: + context.area.tag_redraw() + + if not context.area or context.area.type != "VIEW_3D": + return {"PASS_THROUGH"} + + data = DataStore() + stored_views = context.scene.stored_views + + if len(data.list) > 0 and \ + data.current_index >= 0 and \ + not stored_views.view_modified: + + if not stored_views.view_modified: + sv = data.list[data.current_index] + self.view_name = sv.name + if event.type == 'TIMER': + is_modified = False + if data.mode == 'VIEW': + is_modified = View.is_modified(context, sv) + elif data.mode == 'POV': + is_modified = POV.is_modified(context, sv) + elif data.mode == 'LAYERS': + is_modified = Layers.is_modified(context, sv) + elif data.mode == 'DISPLAY': + is_modified = Display.is_modified(context, sv) + if is_modified: + module_logger.debug( + 'view modified - index: %s name: %s' % (data.current_index, sv.name) + ) + self.view_name = "" + stored_views.view_modified = is_modified + + return {"PASS_THROUGH"} + else: + module_logger.debug('exit') + context.window_manager["stored_views_osd"] = False + VIEW3D_OT_stored_views_draw.handle_remove(context) + + return {'FINISHED'} + + def execute(self, context): + if context.area.type == "VIEW_3D": + self.view_name = "" + VIEW3D_OT_stored_views_draw.handle_add(self, context) + context.window_manager.modal_handler_add(self) + + return {"RUNNING_MODAL"} + else: + self.report({"WARNING"}, "View3D not found. Operation Cancelled") + + return {"CANCELLED"} + +class VIEW3D_OT_stored_views_initialize(Operator): + bl_idname = "view3d.stored_views_initialize" + bl_label = "Initialize" + + @classmethod + def poll(cls, context): + return not hasattr(bpy.types.Scene, 'stored_views') + + def execute(self, context): + bpy.types.Scene.stored_views: PointerProperty( + type=properties.StoredViewsData + ) + scenes = bpy.data.scenes + data = DataStore() + for scene in scenes: + DataStore.sanitize_data(scene) + return {'FINISHED'} + + +from bpy.types import PropertyGroup +from bpy.props import ( + BoolProperty, + BoolVectorProperty, + CollectionProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + IntProperty, + IntVectorProperty, + PointerProperty, + StringProperty, + ) + + +class POVData(PropertyGroup): + distance: FloatProperty() + location: FloatVectorProperty( + subtype='TRANSLATION' + ) + rotation: FloatVectorProperty( + subtype='QUATERNION', + size=4 + ) + name: StringProperty() + perspective: EnumProperty( + items=[('PERSP', '', ''), + ('ORTHO', '', ''), + ('CAMERA', '', '')] + ) + lens: FloatProperty() + clip_start: FloatProperty() + clip_end: FloatProperty() + lock_cursor: BoolProperty() + cursor_location: FloatVectorProperty() + perspective_matrix_md5: StringProperty() + camera_name: StringProperty() + camera_type: StringProperty() + lock_object_name: StringProperty() + + +class LayersData(PropertyGroup): + view_layers: BoolVectorProperty(size=20) + scene_layers: BoolVectorProperty(size=20) + lock_camera_and_layers: BoolProperty() + name: StringProperty() + + +class DisplayData(PropertyGroup): + name: StringProperty() + viewport_shade: EnumProperty( + items=[('BOUNDBOX', 'BOUNDBOX', 'BOUNDBOX'), + ('WIREFRAME', 'WIREFRAME', 'WIREFRAME'), + ('SOLID', 'SOLID', 'SOLID'), + ('TEXTURED', 'TEXTURED', 'TEXTURED'), + ('MATERIAL', 'MATERIAL', 'MATERIAL'), + ('RENDERED', 'RENDERED', 'RENDERED')] + ) + show_only_render: BoolProperty() + show_outline_selected: BoolProperty() + show_all_objects_origin: BoolProperty() + show_relationship_lines: BoolProperty() + show_floor: BoolProperty() + show_axis_x: BoolProperty() + show_axis_y: BoolProperty() + show_axis_z: BoolProperty() + grid_lines: IntProperty() + grid_scale: FloatProperty() + grid_subdivisions: IntProperty() + material_mode: StringProperty() + show_textured_solid: BoolProperty() + quad_view: BoolProperty() + lock_rotation: BoolProperty() + show_sync_view: BoolProperty() + use_box_clip: BoolProperty() + + +class ViewData(PropertyGroup): + pov: PointerProperty( + type=POVData + ) + layers: PointerProperty( + type=LayersData + ) + display: PointerProperty( + type=DisplayData + ) + name: StringProperty() + + +class StoredViewsData(PropertyGroup): + pov_list: CollectionProperty( + type=POVData + ) + layers_list: CollectionProperty( + type=LayersData + ) + display_list: CollectionProperty( + type=DisplayData + ) + view_list: CollectionProperty( + type=ViewData + ) + mode: EnumProperty( + name="Mode", + items=[('VIEW', "View", "3D View settings"), + ('POV', "POV", "POV settings"), + ('LAYERS', "Layers", "Layers settings"), + ('DISPLAY', "Display", "Display settings")], + default='VIEW' + ) + current_indices: IntVectorProperty( + size=4, + default=[-1, -1, -1, -1] + ) + view_modified: BoolProperty( + default=False + ) + +class VIEW3D_OT_stored_views_save(Operator): + bl_idname = "stored_views.save" + bl_label = "Save Current" + bl_description = "Save the view 3d current state" + + index: IntProperty() + + def execute(self, context): + mode = context.scene.stored_views.mode + sv = stored_view_factory(mode, self.index) + sv.save() + context.scene.stored_views.view_modified = False + init_draw(context) + + return {'FINISHED'} + + +class VIEW3D_OT_stored_views_set(Operator): + bl_idname = "stored_views.set" + bl_label = "Set" + bl_description = "Update the view 3D according to this view" + + index: IntProperty() + + def execute(self, context): + mode = context.scene.stored_views.mode + sv = stored_view_factory(mode, self.index) + sv.set() + context.scene.stored_views.view_modified = False + init_draw(context) + + return {'FINISHED'} + + +class VIEW3D_OT_stored_views_delete(Operator): + bl_idname = "stored_views.delete" + bl_label = "Delete" + bl_description = "Delete this view" + + index: IntProperty() + + def execute(self, context): + data = DataStore() + data.delete(self.index) + + return {'FINISHED'} + + +class VIEW3D_OT_New_Camera_to_View(Operator): + bl_idname = "stored_views.newcamera" + bl_label = "New Camera To View" + bl_description = "Add a new Active Camera and align it to this view" + + @classmethod + def poll(cls, context): + return ( + context.space_data is not None and + context.space_data.type == 'VIEW_3D' and + context.space_data.region_3d.view_perspective != 'CAMERA' + ) + + def execute(self, context): + + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + + bpy.ops.object.camera_add() + cam = context.active_object + cam.name = "View_Camera" + # make active camera by hand + context.scene.camera = cam + + bpy.ops.view3d.camera_to_view() + return {'FINISHED'} + + +# Camera marker & switcher by Fsiddi +class VIEW3D_OT_SetSceneCamera(Operator): + bl_idname = "cameraselector.set_scene_camera" + bl_label = "Set Scene Camera" + bl_description = "Set chosen camera as the scene's active camera" + + hide_others = False + + def execute(self, context): + chosen_camera = context.active_object + scene = context.scene + + if self.hide_others: + for c in [o for o in scene.objects if o.type == 'CAMERA']: + c.hide = (c != chosen_camera) + scene.camera = chosen_camera + bpy.ops.object.select_all(action='DESELECT') + chosen_camera.select_set(True) + return {'FINISHED'} + + def invoke(self, context, event): + if event.ctrl: + self.hide_others = True + + return self.execute(context) + + +class VIEW3D_OT_PreviewSceneCamera(Operator): + bl_idname = "cameraselector.preview_scene_camera" + bl_label = "Preview Camera" + bl_description = "Preview chosen camera and make scene's active camera" + + def execute(self, context): + chosen_camera = context.active_object + bpy.ops.view3d.object_as_camera() + bpy.ops.object.select_all(action="DESELECT") + chosen_camera.select_set(True) + return {'FINISHED'} + + +class VIEW3D_OT_AddCameraMarker(Operator): + bl_idname = "cameraselector.add_camera_marker" + bl_label = "Add Camera Marker" + bl_description = "Add a timeline marker bound to chosen camera" + + def execute(self, context): + chosen_camera = context.active_object + scene = context.scene + + current_frame = scene.frame_current + marker = None + for m in reversed(sorted(filter(lambda m: m.frame <= current_frame, + scene.timeline_markers), + key=lambda m: m.frame)): + marker = m + break + if marker and (marker.camera == chosen_camera): + # Cancel if the last marker at or immediately before + # current frame is already bound to the camera. + return {'CANCELLED'} + + marker_name = "F_%02d_%s" % (current_frame, chosen_camera.name) + if marker and (marker.frame == current_frame): + # Reuse existing marker at current frame to avoid + # overlapping bound markers. + marker.name = marker_name + else: + marker = scene.timeline_markers.new(marker_name) + marker.frame = scene.frame_current + marker.camera = chosen_camera + marker.select = True + + for other_marker in [m for m in scene.timeline_markers if m != marker]: + other_marker.select = False + + return {'FINISHED'} + +# gpl authors: nfloyd, Francesco Siddi + + + + + +# TODO: reinstate filters? +class IO_Utils(): + + @staticmethod + def get_preset_path(): + # locate stored_views preset folder + paths = bpy.utils.preset_paths("stored_views") + if not paths: + # stored_views preset folder doesn't exist, so create it + paths = [os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets", + "stored_views")] + if not os.path.exists(paths[0]): + os.makedirs(paths[0]) + + return(paths) + + @staticmethod + def stored_views_apply_from_scene(scene_name, replace=True): + scene = bpy.context.scene + scene_exists = True if scene_name in bpy.data.scenes.keys() else False + + if scene_exists: + sv = bpy.context.scene.stored_views + # io_filters = sv.settings.io_filters + + structs = [sv.view_list, sv.pov_list, sv.layers_list, sv.display_list] + if replace is True: + for st in structs: # clear swap and list + while len(st) > 0: + st.remove(0) + + f_sv = bpy.data.scenes[scene_name].stored_views + # f_sv = bpy.data.scenes[scene_name].stored_views + f_structs = [f_sv.view_list, f_sv.pov_list, f_sv.layers_list, f_sv.display_list] + """ + is_filtered = [io_filters.views, io_filters.point_of_views, + io_filters.layers, io_filters.displays] + """ + for i in range(len(f_structs)): + """ + if is_filtered[i] is False: + continue + """ + for j in f_structs[i]: + item = structs[i].add() + # stored_views_copy_item(j, item) + for k, v in j.items(): + item[k] = v + DataStore.sanitize_data(scene) + return True + else: + return False + + @staticmethod + def stored_views_export_to_blsv(filepath, name='Custom Preset'): + # create dictionary with all information + dump = {"info": {}, "data": {}} + dump["info"]["script"] = bl_info['name'] + dump["info"]["script_version"] = bl_info['version'] + dump["info"]["version"] = bpy.app.version + dump["info"]["preset_name"] = name + + # get current stored views settings + scene = bpy.context.scene + sv = scene.stored_views + + def dump_view_list(dict, list): + if str(type(list)) == "<class 'bpy_prop_collection_idprop'>": + for i, struct_dict in enumerate(list): + dict[i] = {"name": str, + "pov": {}, + "layers": {}, + "display": {}} + dict[i]["name"] = struct_dict.name + dump_item(dict[i]["pov"], struct_dict.pov) + dump_item(dict[i]["layers"], struct_dict.layers) + dump_item(dict[i]["display"], struct_dict.display) + + def dump_list(dict, list): + if str(type(list)) == "<class 'bpy_prop_collection_idprop'>": + for i, struct in enumerate(list): + dict[i] = {} + dump_item(dict[i], struct) + + def dump_item(dict, struct): + for prop in struct.bl_rna.properties: + if prop.identifier == "rna_type": + # not a setting, so skip + continue + + val = getattr(struct, prop.identifier) + if str(type(val)) in ["<class 'bpy_prop_array'>"]: + # array + dict[prop.identifier] = [v for v in val] + # address the pickle limitations of dealing with the Vector class + elif str(type(val)) in ["<class 'Vector'>", + "<class 'Quaternion'>"]: + dict[prop.identifier] = [v for v in val] + else: + # single value + dict[prop.identifier] = val + + # io_filters = sv.settings.io_filters + dump["data"] = {"point_of_views": {}, + "layers": {}, + "displays": {}, + "views": {}} + + others_data = [(dump["data"]["point_of_views"], sv.pov_list), # , io_filters.point_of_views), + (dump["data"]["layers"], sv.layers_list), # , io_filters.layers), + (dump["data"]["displays"], sv.display_list)] # , io_filters.displays)] + for list_data in others_data: + # if list_data[2] is True: + dump_list(list_data[0], list_data[1]) + + views_data = (dump["data"]["views"], sv.view_list) + # if io_filters.views is True: + dump_view_list(views_data[0], views_data[1]) + + # save to file + filepath = filepath + filepath = bpy.path.ensure_ext(filepath, '.blsv') + file = gzip.open(filepath, mode='wb') + pickle.dump(dump, file, protocol=pickle.HIGHEST_PROTOCOL) + file.close() + + @staticmethod + def stored_views_apply_preset(filepath, replace=True): + if not filepath: + return False + + file = gzip.open(filepath, mode='rb') + dump = pickle.load(file) + file.close() + # apply preset + scene = bpy.context.scene + sv = getattr(scene, "stored_views", None) + + if not sv: + return False + + # io_filters = sv.settings.io_filters + sv_data = { + "point_of_views": sv.pov_list, + "views": sv.view_list, + "layers": sv.layers_list, + "displays": sv.display_list + } + for sv_struct, props in dump["data"].items(): + """ + is_filtered = getattr(io_filters, sv_struct) + if is_filtered is False: + continue + """ + sv_list = sv_data[sv_struct] # .list + if replace is True: # clear swap and list + while len(sv_list) > 0: + sv_list.remove(0) + for key, prop_struct in props.items(): + sv_item = sv_list.add() + + for subprop, subval in prop_struct.items(): + if isinstance(subval, dict): # views : pov, layers, displays + v_subprop = getattr(sv_item, subprop) + for v_subkey, v_subval in subval.items(): + if isinstance(v_subval, list): # array like of pov,... + v_array_like = getattr(v_subprop, v_subkey) + for i in range(len(v_array_like)): + v_array_like[i] = v_subval[i] + else: + setattr(v_subprop, v_subkey, v_subval) # others + elif isinstance(subval, list): + array_like = getattr(sv_item, subprop) + for i in range(len(array_like)): + array_like[i] = subval[i] + else: + setattr(sv_item, subprop, subval) + + DataStore.sanitize_data(scene) + + return True + + +class VIEW3D_OT_stored_views_import(Operator, ImportHelper): + bl_idname = "stored_views.import_blsv" + bl_label = "Import Stored Views preset" + bl_description = "Import a .blsv preset file to the current Stored Views" + + filename_ext = ".blsv" + filter_glob: StringProperty( + default="*.blsv", + options={'HIDDEN'} + ) + replace: BoolProperty( + name="Replace", + default=True, + description="Replace current stored views, otherwise append" + ) + + @classmethod + def poll(cls, context): + return get_preferences() + + def execute(self, context): + # the usual way is to not select the file in the file browser + exists = os.path.isfile(self.filepath) if self.filepath else False + if not exists: + self.report({'WARNING'}, + "No filepath specified or file could not be found. Operation Cancelled") + return {'CANCELLED'} + + # apply chosen preset + apply_preset = IO_Utils.stored_views_apply_preset( + filepath=self.filepath, replace=self.replace + ) + if not apply_preset: + self.report({'WARNING'}, + "Please Initialize Stored Views first (in the 3D View Properties Area)") + return {'CANCELLED'} + + # copy preset to presets folder + filename = os.path.basename(self.filepath) + try: + shutil.copyfile(self.filepath, + os.path.join(IO_Utils.get_preset_path()[0], filename)) + except: + self.report({'WARNING'}, + "Stored Views: preset applied, but installing failed (preset already exists?)") + return{'CANCELLED'} + + return{'FINISHED'} + + +class VIEW3D_OT_stored_views_import_from_scene(Operator): + bl_idname = "stored_views.import_from_scene" + bl_label = "Import stored views from scene" + bl_description = "Import currently stored views from an another scene" + + scene_name: StringProperty( + name="Scene Name", + description="A current blend scene", + default="" + ) + replace: BoolProperty( + name="Replace", + default=True, + description="Replace current stored views, otherwise append" + ) + + @classmethod + def poll(cls, context): + return get_preferences() + + def draw(self, context): + layout = self.layout + + layout.prop_search(self, "scene_name", bpy.data, "scenes") + layout.prop(self, "replace") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + # filepath should always be given + if not self.scene_name: + self.report({"WARNING"}, + "No scene name was given. Operation Cancelled") + return{'CANCELLED'} + + is_finished = IO_Utils.stored_views_apply_from_scene( + self.scene_name, replace=self.replace + ) + if not is_finished: + self.report({"WARNING"}, + "Could not find the specified scene. Operation Cancelled") + return {"CANCELLED"} + + return{'FINISHED'} + + +class VIEW3D_OT_stored_views_export(Operator, ExportHelper): + bl_idname = "stored_views.export_blsv" + bl_label = "Export Stored Views preset" + bl_description = "Export the current Stored Views to a .blsv preset file" + + filename_ext = ".blsv" + filepath: StringProperty( + default=os.path.join(IO_Utils.get_preset_path()[0], "untitled") + ) + filter_glob: StringProperty( + default="*.blsv", + options={'HIDDEN'} + ) + preset_name: StringProperty( + name="Preset name", + default="", + description="Name of the stored views preset" + ) + + @classmethod + def poll(cls, context): + return get_preferences() + + def execute(self, context): + IO_Utils.stored_views_export_to_blsv(self.filepath, self.preset_name) + + return{'FINISHED'} + + +class VIEW3D_PT_properties_stored_views(Panel): + bl_label = "Stored Views" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "View" + + def draw(self, context): + self.logger = logging.getLogger('%s Properties panel' % __name__) + layout = self.layout + + if bpy.ops.view3d.stored_views_initialize.poll(): + layout.operator("view3d.stored_views_initialize") + return + + stored_views = context.scene.stored_views + + # UI : mode + col = layout.column(align=True) + col.prop_enum(stored_views, "mode", 'VIEW') + row = layout.row(align=True) + row.operator("view3d.camera_to_view", text="Camera To view") + row.operator("stored_views.newcamera") + + row = col.row(align=True) + row.prop_enum(stored_views, "mode", 'POV') + row.prop_enum(stored_views, "mode", 'LAYERS') + row.prop_enum(stored_views, "mode", 'DISPLAY') + + # UI : operators + row = layout.row() + row.operator("stored_views.save").index = -1 + + # IO Operators + if core.get_preferences(): + row = layout.row(align=True) + row.operator("stored_views.import_from_scene", text="Import from Scene") + row.operator("stored_views.import_blsv", text="", icon="IMPORT") + row.operator("stored_views.export_blsv", text="", icon="EXPORT") + + data_store = DataStore() + list = data_store.list + # UI : items list + if len(list) > 0: + row = layout.row() + box = row.box() + # items list + mode = stored_views.mode + for i in range(len(list)): + # associated icon + icon_string = "MESH_CUBE" # default icon + # TODO: icons for view + if mode == 'POV': + persp = list[i].perspective + if persp == 'PERSP': + icon_string = "MESH_CUBE" + elif persp == 'ORTHO': + icon_string = "MESH_PLANE" + elif persp == 'CAMERA': + if list[i].camera_type != 'CAMERA': + icon_string = 'OBJECT_DATAMODE' + else: + icon_string = "OUTLINER_DATA_CAMERA" + if mode == 'LAYERS': + if list[i].lock_camera_and_layers is True: + icon_string = 'SCENE_DATA' + else: + icon_string = 'RENDERLAYERS' + if mode == 'DISPLAY': + shade = list[i].viewport_shade + if shade == 'TEXTURED': + icon_string = 'TEXTURE_SHADED' + if shade == 'MATERIAL': + icon_string = 'MATERIAL_DATA' + elif shade == 'SOLID': + icon_string = 'SOLID' + elif shade == 'WIREFRAME': + icon_string = "WIRE" + elif shade == 'BOUNDBOX': + icon_string = 'BBOX' + elif shade == 'RENDERED': + icon_string = 'MATERIAL' + # stored view row + subrow = box.row(align=True) + # current view indicator + if data_store.current_index == i and context.scene.stored_views.view_modified is False: + subrow.label(text="", icon='SMALL_TRI_RIGHT_VEC') + subrow.operator("stored_views.set", + text="", icon=icon_string).index = i + subrow.prop(list[i], "name", text="") + subrow.operator("stored_views.save", + text="", icon="REC").index = i + subrow.operator("stored_views.delete", + text="", icon="PANEL_CLOSE").index = i + + layout = self.layout + scene = context.scene + layout.label(text="Camera Selector") + cameras = sorted([o for o in scene.objects if o.type == 'CAMERA'], + key=lambda o: o.name) + + if len(cameras) > 0: + for camera in cameras: + row = layout.row(align=True) + row.context_pointer_set("active_object", camera) + row.operator("cameraselector.set_scene_camera", + text=camera.name, icon='OUTLINER_DATA_CAMERA') + row.operator("cameraselector.preview_scene_camera", + text='', icon='RESTRICT_VIEW_OFF') + row.operator("cameraselector.add_camera_marker", + text='', icon='MARKER') + else: + layout.label(text="No cameras in this scene") +# Addon Preferences + +class VIEW3D_OT_stored_views_preferences(AddonPreferences): + bl_idname = __name__ + + show_exporters: BoolProperty( + name="Enable I/O Operators", + default=False, + description="Enable Import/Export Operations in the UI:\n" + "Import Stored Views preset,\n" + "Export Stored Views preset and \n" + "Import stored views from scene", + ) + view_3d_update_rate: IntProperty( + name="3D view update", + description="Update rate of the 3D view redraw\n" + "Increse the value if the UI feels sluggish", + min=1, max=10, + default=1 + ) + + def draw(self, context): + layout = self.layout + + row = layout.row(align=True) + row.prop(self, "view_3d_update_rate", toggle=True) + row.prop(self, "show_exporters", toggle=True) + + +# Register +classes = [ + VIEW3D_OT_stored_views_initialize, + VIEW3D_OT_stored_views_preferences, + VIEW3D_PT_properties_stored_views, + POVData, + LayersData, + DisplayData, + ViewData, + StoredViewsData, + VIEW3D_OT_stored_views_draw, + VIEW3D_OT_stored_views_save, + VIEW3D_OT_stored_views_set, + VIEW3D_OT_stored_views_delete, + VIEW3D_OT_New_Camera_to_View, + VIEW3D_OT_SetSceneCamera, + VIEW3D_OT_PreviewSceneCamera, + VIEW3D_OT_AddCameraMarker, +# IO_Utils, + VIEW3D_OT_stored_views_import, + VIEW3D_OT_stored_views_import_from_scene, + VIEW3D_OT_stored_views_export + ] + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + +def unregister(): + ui.VIEW3D_OT_stored_views_draw.handle_remove(bpy.context) + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + if hasattr(bpy.types.Scene, "stored_views"): + del bpy.types.Scene.stored_views + + +if __name__ == "__main__": + register() diff --git a/space_view3d_stored_views/ui.py b/space_view3d_stored_views/ui.py new file mode 100644 index 00000000..43f3d470 --- /dev/null +++ b/space_view3d_stored_views/ui.py @@ -0,0 +1,277 @@ +# gpl authors: nfloyd, Francesco Siddi + +import logging +module_logger = logging.getLogger(__name__) + +import bpy +import blf +from . import core +from bpy.types import ( + Operator, + Panel, +) + +""" + If view name display is enabled, + it will check periodically if the view has been modified + since last set. + get_preferences_timer() is the time in seconds between these checks. + It can be increased, if the view become sluggish + It is set in the add-on preferences +""" + + +# Utility function get_preferences_timer for update of 3d view draw +def get_preferences_timer(): + # replace the key if the add-on name changes + # TODO: expose refresh rate to ui??? + addon = bpy.context.preferences.addons[__package__] + timer_update = (addon.preferences.view_3d_update_rate if addon else False) + + return timer_update + + +def init_draw(context=None): + if context is None: + context = bpy.context + + if "stored_views_osd" not in context.window_manager: + context.window_manager["stored_views_osd"] = False + + if not context.window_manager["stored_views_osd"]: + context.window_manager["stored_views_osd"] = True + bpy.ops.stored_views.draw() + + +def _draw_callback_px(self, context): + if context.area and context.area.type == 'VIEW_3D': + r_width = text_location = context.region.width + r_height = context.region.height + font_id = 0 # TODO: need to find out how best to get font_id + + blf.size(font_id, 11, context.preferences.system.dpi) + text_size = blf.dimensions(0, self.view_name) + + # compute the text location + text_location = 0 + overlap = context.preferences.system.use_region_overlap + if overlap: + for region in context.area.regions: + if region.type == "UI": + text_location = r_width - region.width + + text_x = text_location - text_size[0] - 10 + text_y = r_height - text_size[1] - 8 + blf.position(font_id, text_x, text_y, 0) + blf.draw(font_id, self.view_name) + + +class VIEW3D_stored_views_draw(Operator): + bl_idname = "stored_views.draw" + bl_label = "Show current" + bl_description = "Toggle the display current view name in the view 3D" + + _handle = None + _timer = None + + @staticmethod + def handle_add(self, context): + VIEW3D_stored_views_draw._handle = bpy.types.SpaceView3D.draw_handler_add( + _draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') + VIEW3D_stored_views_draw._timer = \ + context.window_manager.event_timer_add(get_preferences_timer(), window=context.window) + + @staticmethod + def handle_remove(context): + if VIEW3D_stored_views_draw._handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(VIEW3D_stored_views_draw._handle, 'WINDOW') + if VIEW3D_stored_views_draw._timer is not None: + context.window_manager.event_timer_remove(VIEW3D_stored_views_draw._timer) + VIEW3D_stored_views_draw._handle = None + VIEW3D_stored_views_draw._timer = None + + @classmethod + def poll(cls, context): + # return context.mode == 'OBJECT' + return True + + def modal(self, context, event): + if context.area: + context.area.tag_redraw() + + if not context.area or context.area.type != "VIEW_3D": + return {"PASS_THROUGH"} + + data = core.DataStore() + stored_views = context.scene.stored_views + + if len(data.list) > 0 and \ + data.current_index >= 0 and \ + not stored_views.view_modified: + + if not stored_views.view_modified: + sv = data.list[data.current_index] + self.view_name = sv.name + if event.type == 'TIMER': + is_modified = False + if data.mode == 'VIEW': + is_modified = core.View.is_modified(context, sv) + elif data.mode == 'POV': + is_modified = core.POV.is_modified(context, sv) + elif data.mode == 'LAYERS': + is_modified = core.Layers.is_modified(context, sv) + elif data.mode == 'DISPLAY': + is_modified = core.Display.is_modified(context, sv) + if is_modified: + module_logger.debug( + 'view modified - index: %s name: %s' % (data.current_index, sv.name) + ) + self.view_name = "" + stored_views.view_modified = is_modified + + return {"PASS_THROUGH"} + else: + module_logger.debug('exit') + context.window_manager["stored_views_osd"] = False + VIEW3D_stored_views_draw.handle_remove(context) + + return {'FINISHED'} + + def execute(self, context): + if context.area.type == "VIEW_3D": + self.view_name = "" + VIEW3D_stored_views_draw.handle_add(self, context) + context.window_manager.modal_handler_add(self) + + return {"RUNNING_MODAL"} + else: + self.report({"WARNING"}, "View3D not found. Operation Cancelled") + + return {"CANCELLED"} + + +class VIEW3D_PT_properties_stored_views(Panel): + bl_label = "Stored Views" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "StoredViews" + + def draw(self, context): + self.logger = logging.getLogger('%s Properties panel' % __name__) + layout = self.layout + + if bpy.ops.view3d.stored_views_initialize.poll(): + layout.operator("view3d.stored_views_initialize") + return + + stored_views = context.scene.stored_views + + # UI : mode + col = layout.column(align=True) + col.prop_enum(stored_views, "mode", 'VIEW') + row = layout.row(align=True) + row.operator("view3d.camera_to_view", text="Camera To view") + row.operator("stored_views.newcamera") + + row = col.row(align=True) + row.prop_enum(stored_views, "mode", 'POV') + # row.prop_enum(stored_views, "mode", 'LAYERS') + # row.prop_enum(stored_views, "mode", 'DISPLAY') + + # UI : operators + row = layout.row() + row.operator("stored_views.save").index = -1 + + # IO Operators + if core.get_preferences(): + row = layout.row(align=True) + row.operator("stored_views.import_from_scene", text="Import from Scene") + row.operator("stored_views.import_blsv", text="", icon="IMPORT") + row.operator("stored_views.export_blsv", text="", icon="EXPORT") + + data_store = core.DataStore() + list = data_store.list + # UI : items list + if len(list) > 0: + row = layout.row() + box = row.box() + # items list + mode = stored_views.mode + for i in range(len(list)): + # associated icon + icon_string = "MESH_CUBE" # default icon + # TODO: icons for view + if mode == 'POV': + persp = list[i].perspective + if persp == 'PERSP': + icon_string = "MESH_CUBE" + elif persp == 'ORTHO': + icon_string = "MESH_PLANE" + elif persp == 'CAMERA': + if list[i].camera_type != 'CAMERA': + icon_string = 'OBJECT_DATAMODE' + else: + icon_string = "OUTLINER_DATA_CAMERA" + if mode == 'LAYERS': + if list[i].lock_camera_and_layers is True: + icon_string = 'SCENE_DATA' + else: + icon_string = 'RENDERLAYERS' + if mode == 'DISPLAY': + shade = list[i].viewport_shade + if shade == 'TEXTURED': + icon_string = 'TEXTURE_SHADED' + if shade == 'MATERIAL': + icon_string = 'MATERIAL_DATA' + elif shade == 'SOLID': + icon_string = 'SOLID' + elif shade == 'WIREFRAME': + icon_string = "WIRE" + elif shade == 'BOUNDBOX': + icon_string = 'BBOX' + elif shade == 'RENDERED': + icon_string = 'MATERIAL' + # stored view row + subrow = box.row(align=True) + # current view indicator + if data_store.current_index == i and context.scene.stored_views.view_modified is False: + subrow.label(text="", icon='SMALL_TRI_RIGHT_VEC') + subrow.operator("stored_views.set", + text="", icon=icon_string).index = i + subrow.prop(list[i], "name", text="") + subrow.operator("stored_views.save", + text="", icon="REC").index = i + subrow.operator("stored_views.delete", + text="", icon="PANEL_CLOSE").index = i + + layout = self.layout + scene = context.scene + layout.label(text="Camera Selector") + cameras = sorted([o for o in scene.objects if o.type == 'CAMERA'], + key=lambda o: o.name) + + if len(cameras) > 0: + for camera in cameras: + row = layout.row(align=True) + row.context_pointer_set("active_object", camera) + row.operator("cameraselector.set_scene_camera", + text=camera.name, icon='OUTLINER_DATA_CAMERA') + row.operator("cameraselector.preview_scene_camera", + text='', icon='RESTRICT_VIEW_OFF') + row.operator("cameraselector.add_camera_marker", + text='', icon='MARKER') + else: + layout.label(text="No cameras in this scene") + +classes = ( + VIEW3D_stored_views_draw, + VIEW3D_PT_properties_stored_views +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) |