diff --git a/multi_user/__init__.py b/multi_user/__init__.py index f27d541..1015e79 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + bl_info = { "name": "Multi-User", "author": "Swann Martinez", diff --git a/multi_user/bl_types/__init__.py b/multi_user/bl_types/__init__.py index c3e9605..b5427c2 100644 --- a/multi_user/bl_types/__init__.py +++ b/multi_user/bl_types/__init__.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + __all__ = [ 'bl_object', 'bl_mesh', diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 9d4c28f..2ca2cf3 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -1,11 +1,200 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils import copy +import numpy as np +from enum import Enum from .. import utils from .bl_datablock import BlDatablock -# WIP + +ENUM_EASING_TYPE = [ + 'AUTO', + 'EAS_IN', + 'EASE_OUT', + 'EASE_IN_OUT'] + + +ENUM_HANDLE_TYPE = [ + 'FREE', + 'ALIGNED', + 'VECTOR', + 'AUTO', + 'AUTO_CLAMPED'] + + +ENUM_INTERPOLATION_TYPE = [ + 'CONSTANT', + 'LINEAR', + 'BEZIER', + 'SINE', + 'QUAD', + 'CUBIC', + 'QUART', + 'QUINT', + 'EXPO', + 'CIRC', + 'BACK', + 'BOUNCE', + 'ELASTIC'] + + +ENUM_KEY_TYPE = [ + 'KEYFRAME', + 'BREAKDOWN', + 'MOVING_HOLD', + 'EXTREME', + 'JITTER'] + + +#TODO: Automatic enum and numpy dump and loading + + +def dump_fcurve(fcurve, use_numpy=True): + """ Dump a sigle curve to a dict + + :arg fcurve: fcurve to dump + :type fcurve: bpy.types.FCurve + :arg use_numpy: use numpy to eccelerate dump + :type use_numpy: bool + :return: dict + """ + fcurve_data = { + "data_path": fcurve.data_path, + "dumped_array_index": fcurve.array_index, + "use_numpy": use_numpy + } + + if use_numpy: + keyframes_count = len(fcurve.keyframe_points) + + k_amplitude = np.empty(keyframes_count, dtype=np.float64) + fcurve.keyframe_points.foreach_get('amplitude', k_amplitude) + k_co = np.empty(keyframes_count*2, dtype=np.float64) + fcurve.keyframe_points.foreach_get('co', k_co) + k_back = np.empty(keyframes_count, dtype=np.float64) + fcurve.keyframe_points.foreach_get('back', k_back) + k_handle_left = np.empty(keyframes_count*2, dtype=np.float64) + fcurve.keyframe_points.foreach_get('handle_left', k_handle_left) + k_handle_right = np.empty(keyframes_count*2, dtype=np.float64) + fcurve.keyframe_points.foreach_get('handle_right', k_handle_right) + + fcurve_data['amplitude'] = k_amplitude.tobytes() + fcurve_data['co'] = k_co.tobytes() + fcurve_data['back'] = k_back.tobytes() + fcurve_data['handle_left'] = k_handle_left.tobytes() + fcurve_data['handle_right'] = k_handle_right.tobytes() + + fcurve_data['easing'] = [ENUM_EASING_TYPE.index(p.easing) for p in fcurve.keyframe_points] + fcurve_data['handle_left_type'] = [ENUM_HANDLE_TYPE.index(p.handle_left_type) for p in fcurve.keyframe_points] + fcurve_data['handle_right_type'] = [ENUM_HANDLE_TYPE.index(p.handle_right_type) for p in fcurve.keyframe_points] + fcurve_data['type'] = [ENUM_KEY_TYPE.index(p.type) for p in fcurve.keyframe_points] + fcurve_data['interpolation'] = [ENUM_INTERPOLATION_TYPE.index(p.interpolation) for p in fcurve.keyframe_points] + + else: # Legacy method + dumper = utils.dump_anything.Dumper() + fcurve_data["keyframe_points"] = [] + + for k in fcurve.keyframe_points: + fcurve_data["keyframe_points"].append( + dumper.dump(k) + ) + + return fcurve_data + +def load_fcurve(fcurve_data, fcurve): + """ Load a dumped fcurve + + :arg fcurve_data: a dumped fcurve + :type fcurve_data: dict + :arg fcurve: fcurve to dump + :type fcurve: bpy.types.FCurve + """ + use_numpy = fcurve_data.get('use_numpy') + + keyframe_points = fcurve.keyframe_points + + # Remove all keyframe points + for i in range(len(keyframe_points)): + keyframe_points.remove(keyframe_points[0], fast=True) + + if use_numpy: + k_amplitude = np.frombuffer(fcurve_data['amplitude'], dtype=np.float64) + + keyframe_count = len(k_amplitude) + + k_co = np.frombuffer(fcurve_data['co'], dtype=np.float64) + k_back = np.frombuffer(fcurve_data['back'], dtype=np.float64) + k_amplitude = np.frombuffer(fcurve_data['amplitude'], dtype=np.float64) + k_handle_left= np.frombuffer(fcurve_data['handle_left'], dtype=np.float64) + k_handle_right= np.frombuffer(fcurve_data['handle_right'], dtype=np.float64) + + keyframe_points.add(keyframe_count) + + keyframe_points.foreach_set('co',k_co) + keyframe_points.foreach_set('back',k_back) + keyframe_points.foreach_set('amplitude',k_amplitude) + keyframe_points.foreach_set('handle_left',k_handle_left) + keyframe_points.foreach_set('handle_right',k_handle_right) + + for index, point in enumerate(keyframe_points): + point.type = ENUM_KEY_TYPE[fcurve_data['type'][index]] + point.easing = ENUM_EASING_TYPE[fcurve_data['easing'][index]] + point.handle_left_type = ENUM_HANDLE_TYPE[fcurve_data['handle_left_type'][index]] + point.handle_right_type = ENUM_HANDLE_TYPE[fcurve_data['handle_right_type'][index]] + point.interpolation = ENUM_INTERPOLATION_TYPE[fcurve_data['interpolation'][index]] + + else: + # paste dumped keyframes + for dumped_keyframe_point in fcurve_data["keyframe_points"]: + if dumped_keyframe_point['type'] == '': + dumped_keyframe_point['type'] = 'KEYFRAME' + + new_kf = keyframe_points.insert( + dumped_keyframe_point["co"][0], + dumped_keyframe_point["co"][1], + options={'FAST', 'REPLACE'} + ) + + keycache = copy.copy(dumped_keyframe_point) + keycache = utils.dump_anything.remove_items_from_dict( + keycache, + ["co", "handle_left", "handle_right", 'type'] + ) + + utils.dump_anything.load(new_kf, keycache) + + new_kf.type = dumped_keyframe_point['type'] + new_kf.handle_left = [ + dumped_keyframe_point["handle_left"][0], + dumped_keyframe_point["handle_left"][1] + ] + new_kf.handle_right = [ + dumped_keyframe_point["handle_right"][0], + dumped_keyframe_point["handle_right"][1] + ] + + fcurve.update() + + class BlAction(BlDatablock): bl_id = "actions" @@ -14,30 +203,11 @@ class BlAction(BlDatablock): bl_delay_apply = 1 bl_automatic_push = True bl_icon = 'ACTION_TWEAK' - - def construct(self, data): + + def _construct(self, data): return bpy.data.actions.new(data["name"]) - def load(self, data, target): - begin_frame = 100000 - end_frame = -100000 - - for dumped_fcurve in data["fcurves"]: - begin_frame = min( - begin_frame, - min( - [begin_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]] - ) - ) - end_frame = max( - end_frame, - max( - [end_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]] - ) - ) - begin_frame = 0 - - loader = utils.dump_anything.Loader() + def _load(self, data, target): for dumped_fcurve in data["fcurves"]: dumped_data_path = dumped_fcurve["data_path"] dumped_array_index = dumped_fcurve["dumped_array_index"] @@ -47,53 +217,13 @@ class BlAction(BlDatablock): if fcurve is None: fcurve = target.fcurves.new(dumped_data_path, index=dumped_array_index) + load_fcurve(dumped_fcurve, fcurve) + target.id_root = data['id_root'] - # remove keyframes within dumped_action range - for keyframe in reversed(fcurve.keyframe_points): - if end_frame >= (keyframe.co[0] + begin_frame ) >= begin_frame: - fcurve.keyframe_points.remove(keyframe, fast=True) - - # paste dumped keyframes - for dumped_keyframe_point in dumped_fcurve["keyframe_points"]: - if dumped_keyframe_point['type'] == '': - dumped_keyframe_point['type'] = 'KEYFRAME' - - new_kf = fcurve.keyframe_points.insert( - dumped_keyframe_point["co"][0] - begin_frame, - dumped_keyframe_point["co"][1], - options={'FAST', 'REPLACE'} - ) - - keycache = copy.copy(dumped_keyframe_point) - keycache = utils.dump_anything.remove_items_from_dict( - keycache, - ["co", "handle_left", "handle_right",'type'] - ) - - loader.load( - new_kf, - keycache - ) - - new_kf.type = dumped_keyframe_point['type'] - new_kf.handle_left = [ - dumped_keyframe_point["handle_left"][0] - begin_frame, - dumped_keyframe_point["handle_left"][1] - ] - new_kf.handle_right = [ - dumped_keyframe_point["handle_right"][0] - begin_frame, - dumped_keyframe_point["handle_right"][1] - ] - - # clearing (needed for blender to update well) - if len(fcurve.keyframe_points) == 0: - target.fcurves.remove(fcurve) - target.id_root= data['id_root'] - - def dump(self, pointer=None): + def _dump(self, pointer=None): assert(pointer) dumper = utils.dump_anything.Dumper() - dumper.exclude_filter =[ + dumper.exclude_filter = [ 'name_full', 'original', 'use_fake_user', @@ -106,27 +236,13 @@ class BlAction(BlDatablock): 'users' ] dumper.depth = 1 - data = dumper.dump(pointer) + data = dumper.dump(pointer) - data["fcurves"] = [] - dumper.depth = 2 + for fcurve in self.pointer.fcurves: - fc = { - "data_path": fcurve.data_path, - "dumped_array_index": fcurve.array_index, - "keyframe_points": [] - } + data["fcurves"].append(dump_fcurve(fcurve, use_numpy=True)) - for k in fcurve.keyframe_points: - fc["keyframe_points"].append( - dumper.dump(k) - ) - - data["fcurves"].append(fc) return data - - - diff --git a/multi_user/bl_types/bl_armature.py b/multi_user/bl_types/bl_armature.py index 8aca65c..81d464f 100644 --- a/multi_user/bl_types/bl_armature.py +++ b/multi_user/bl_types/bl_armature.py @@ -1,13 +1,28 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils -from ..libs.overrider import Overrider from .. import utils from .. import presence, operators from .bl_datablock import BlDatablock -# WIP - class BlArmature(BlDatablock): bl_id = "armatures" @@ -17,7 +32,7 @@ class BlArmature(BlDatablock): bl_automatic_push = True bl_icon = 'ARMATURE_DATA' - def construct(self, data): + def _construct(self, data): return bpy.data.armatures.new(data["name"]) def load_implementation(self, data, target): diff --git a/multi_user/bl_types/bl_camera.py b/multi_user/bl_types/bl_camera.py index 72700a2..4876010 100644 --- a/multi_user/bl_types/bl_camera.py +++ b/multi_user/bl_types/bl_camera.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -13,6 +31,10 @@ class BlCamera(BlDatablock): bl_automatic_push = True bl_icon = 'CAMERA_DATA' + def _construct(self, data): + return bpy.data.cameras.new(data["name"]) + + def load_implementation(self, data, target): utils.dump_anything.load(target, data) @@ -22,12 +44,11 @@ class BlCamera(BlDatablock): if dof_settings: utils.dump_anything.load(target.dof, dof_settings) - def construct(self, data): - return bpy.data.cameras.new(data["name"]) - def dump_implementation(self, data, pointer=None): assert(pointer) + # TODO: background image support + dumper = utils.dump_anything.Dumper() dumper.depth = 2 dumper.include_filter = [ @@ -49,6 +70,14 @@ class BlCamera(BlDatablock): 'aperture_blades', 'aperture_rotation', 'aperture_ratio', + 'display_size', + 'show_limits', + 'show_mist', + 'show_sensor', + 'show_name', + 'sensor_fit', + 'sensor_height', + 'sensor_width', ] return dumper.dump(pointer) diff --git a/multi_user/bl_types/bl_collection.py b/multi_user/bl_types/bl_collection.py index 38ac0b1..3d3f9f7 100644 --- a/multi_user/bl_types/bl_collection.py +++ b/multi_user/bl_types/bl_collection.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -13,7 +31,7 @@ class BlCollection(BlDatablock): bl_delay_apply = 1 bl_automatic_push = True - def construct(self, data): + def _construct(self, data): if self.is_library: with bpy.data.libraries.load(filepath=bpy.data.libraries[self.data['library']].filepath, link=True) as (sourceData, targetData): targetData.collections = [ @@ -28,7 +46,7 @@ class BlCollection(BlDatablock): instance.uuid = self.uuid return instance - def load(self, data, target): + def load_implementation(self, data, target): # Load other meshes metadata # dump_anything.load(target, data) target.name = data["name"] diff --git a/multi_user/bl_types/bl_curve.py b/multi_user/bl_types/bl_curve.py index 4a498e9..ac6fbdc 100644 --- a/multi_user/bl_types/bl_curve.py +++ b/multi_user/bl_types/bl_curve.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import bpy.types as T import mathutils @@ -15,7 +33,7 @@ class BlCurve(BlDatablock): bl_automatic_push = True bl_icon = 'CURVE_DATA' - def construct(self, data): + def _construct(self, data): return bpy.data.curves.new(data["name"], data["type"]) def load_implementation(self, data, target): diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index 60c0a86..3a5d8b3 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -61,7 +79,6 @@ class BlDatablock(ReplicatedDatablock): bl_automatic_push : boolean bl_icon : type icon (blender icon name) """ - bl_id = "scenes" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -77,23 +94,7 @@ class BlDatablock(ReplicatedDatablock): self.diff_method = DIFF_BINARY - def library_apply(self): - """Apply stored data - """ - # UP in case we want to reset our pointer data - self.state = UP - - def bl_diff(self): - """Generic datablock diff""" - return self.pointer.name != self.data['name'] - - def diff_library(self): - return False - - def resolve_deps_library(self): - return [self.pointer.library] - - def resolve(self): + def _resolve(self): datablock_ref = None datablock_root = getattr(bpy.data, self.bl_id) datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) @@ -108,7 +109,7 @@ class BlDatablock(ReplicatedDatablock): self.pointer = datablock_ref - def dump(self, pointer=None): + def _dump(self, pointer=None): data = {} # Dump animation data if utils.has_action(pointer): @@ -134,7 +135,7 @@ class BlDatablock(ReplicatedDatablock): def dump_implementation(self, data, target): raise NotImplementedError - def load(self, data, target): + def _load(self, data, target): # Load animation data if 'animation_data' in data.keys(): if target.animation_data is None: diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index d593e6d..79ada32 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -1,72 +1,271 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils +import numpy as np from ..libs import dump_anything from .bl_datablock import BlDatablock +# GPencil data api is structured as it follow: +# GP-Object --> GP-Layers --> GP-Frames --> GP-Strokes --> GP-Stroke-Points -def load_gpencil_layer(target=None, data=None, create=False): +def dump_stroke(stroke): + """ Dump a grease pencil stroke to a dict - dump_anything.load(target, data) - for k,v in target.frames.items(): - target.frames.remove(v) - - for frame in data["frames"]: - - tframe = target.frames.new(data["frames"][frame]['frame_number']) + :param stroke: target grease pencil stroke + :type stroke: bpy.types.GPencilStroke + :return: dict + """ - for stroke in data["frames"][frame]["strokes"]: - try: - tstroke = tframe.strokes[stroke] - except: - tstroke = tframe.strokes.new() - dump_anything.load( - tstroke, data["frames"][frame]["strokes"][stroke]) + assert(stroke) - for point in data["frames"][frame]["strokes"][stroke]["points"]: - p = data["frames"][frame]["strokes"][stroke]["points"][point] + dumper = dump_anything.Dumper() + dumper.include_filter = [ + "aspect", + "display_mode", + "draw_cyclic", + "end_cap_mode", + "hardeness", + "line_width", + "material_index", + "start_cap_mode", + "uv_rotation", + "uv_scale", + "uv_translation", + "vertex_color_fill", + ] + dumped_stroke = dumper.dump(stroke) - tstroke.points.add(1) - tpoint = tstroke.points[len(tstroke.points)-1] + # Stoke points + p_count = len(stroke.points) + dumped_stroke['p_count'] = p_count + + p_co = np.empty(p_count*3, dtype=np.float64) + stroke.points.foreach_get('co', p_co) + dumped_stroke['p_co'] = p_co.tobytes() + + p_pressure = np.empty(p_count, dtype=np.float64) + stroke.points.foreach_get('pressure', p_pressure) + dumped_stroke['p_pressure'] = p_pressure.tobytes() + + p_strength = np.empty(p_count, dtype=np.float64) + stroke.points.foreach_get('strength', p_strength) + dumped_stroke['p_strength'] = p_strength.tobytes() + + if bpy.app.version[1] >= 83: # new in blender 2.83 + p_vertex_color = np.empty(p_count*4, dtype=np.float64) + stroke.points.foreach_get('vertex_color', p_vertex_color) + dumped_stroke['p_vertex_color'] = p_vertex_color.tobytes() + + # TODO: uv_factor, uv_rotation + + return dumped_stroke + + +def load_stroke(stroke_data, stroke): + """ Load a grease pencil stroke from a dict + + :param stroke_data: dumped grease pencil stroke + :type stroke_data: dict + :param stroke: target grease pencil stroke + :type stroke: bpy.types.GPencilStroke + """ + assert(stroke and stroke_data) + + dump_anything.load(stroke, stroke_data) + + p_co = np.frombuffer(stroke_data["p_co"], dtype=np.float64) + p_pressure = np.frombuffer(stroke_data["p_pressure"], dtype=np.float64) + p_strength = np.frombuffer(stroke_data["p_strength"], dtype=np.float64) + + stroke.points.add(stroke_data["p_count"]) + + stroke.points.foreach_set('co', p_co) + stroke.points.foreach_set('pressure', p_pressure) + stroke.points.foreach_set('strength', p_strength) + + if "p_vertex_color" in stroke_data: + p_vertex_color = np.frombuffer(stroke_data["p_vertex_color"], dtype=np.float64) + stroke.points.foreach_set('vertex_color', p_vertex_color) + + +def dump_frame(frame): + """ Dump a grease pencil frame to a dict + + :param frame: target grease pencil stroke + :type frame: bpy.types.GPencilFrame + :return: dict + """ + + assert(frame) + + dumped_frame = dict() + dumped_frame['frame_number'] = frame.frame_number + dumped_frame['strokes'] = [] + + # TODO: took existing strokes in account + for stroke in frame.strokes: + dumped_frame['strokes'].append(dump_stroke(stroke)) + + return dumped_frame + + +def load_frame(frame_data, frame): + """ Load a grease pencil frame from a dict + + :param frame_data: source grease pencil frame + :type frame_data: dict + :param frame: target grease pencil stroke + :type frame: bpy.types.GPencilFrame + """ + + assert(frame and frame_data) + + # frame.frame_number = frame_data['frame_number'] + + # TODO: took existing stroke in account + + for stroke_data in frame_data['strokes']: + target_stroke = frame.strokes.new() + load_stroke(stroke_data, target_stroke) + + +def dump_layer(layer): + """ Dump a grease pencil layer + + :param layer: target grease pencil stroke + :type layer: bpy.types.GPencilFrame + """ + + assert(layer) + + dumper = dump_anything.Dumper() + + dumper.include_filter = [ + 'info', + 'opacity', + 'channel_color', + 'color', + # 'thickness', + 'tint_color', + 'tint_factor', + 'vertex_paint_opacity', + 'line_change', + 'use_onion_skinning', + # 'use_annotation_onion_skinning', + # 'annotation_onion_before_range', + # 'annotation_onion_after_range', + # 'annotation_onion_before_color', + # 'annotation_onion_after_color', + 'pass_index', + # 'viewlayer_render', + 'blend_mode', + 'hide', + 'annotation_hide', + 'lock', + # 'lock_frame', + # 'lock_material', + # 'use_mask_layer', + 'use_lights', + 'use_solo_mode', + 'select', + 'show_points', + 'show_in_front', + # 'parent', + # 'parent_type', + # 'parent_bone', + # 'matrix_inverse', + ] + dumped_layer = dumper.dump(layer) + + dumped_layer['frames'] = [] + + for frame in layer.frames: + dumped_layer['frames'].append(dump_frame(frame)) + + return dumped_layer + + +def load_layer(layer_data, layer): + """ Load a grease pencil layer from a dict + + :param layer_data: source grease pencil layer data + :type layer_data: dict + :param layer: target grease pencil stroke + :type layer: bpy.types.GPencilFrame + """ + # TODO: take existing data in account + dump_anything.load(layer, layer_data) + + for frame_data in layer_data["frames"]: + target_frame = layer.frames.new(frame_data['frame_number']) + + load_frame(frame_data, target_frame) - dump_anything.load(tpoint, p) class BlGpencil(BlDatablock): bl_id = "grease_pencils" bl_class = bpy.types.GreasePencil - bl_delay_refresh = 5 - bl_delay_apply = 5 + bl_delay_refresh = 2 + bl_delay_apply = 1 bl_automatic_push = True bl_icon = 'GREASEPENCIL' - def construct(self, data): + def _construct(self, data): return bpy.data.grease_pencils.new(data["name"]) def load_implementation(self, data, target): - for layer in target.layers: - target.layers.remove(layer) - - if "layers" in data.keys(): - for layer in data["layers"]: - if layer not in target.layers.keys(): - gp_layer = target.layers.new(data["layers"][layer]["info"]) - else: - gp_layer = target.layers[layer] - load_gpencil_layer( - target=gp_layer, data=data["layers"][layer], create=True) - - dump_anything.load(target, data) - target.materials.clear() if "materials" in data.keys(): for mat in data['materials']: target.materials.append(bpy.data.materials[mat]) + # TODO: reuse existing layer + for layer in target.layers: + target.layers.remove(layer) + + if "layers" in data.keys(): + for layer in data["layers"]: + layer_data = data["layers"].get(layer) + + # if layer not in target.layers.keys(): + target_layer = target.layers.new(data["layers"][layer]["info"]) + # else: + # target_layer = target.layers[layer] + # target_layer.clear() + + load_layer(layer_data, target_layer) + + dump_anything.load(target, data) + + + def dump_implementation(self, data, pointer=None): assert(pointer) data = dump_anything.dump(pointer, 2) - data['layers'] = dump_anything.dump(pointer.layers, 9) + + data['layers'] = {} + + for layer in pointer.layers: + data['layers'][layer.info] = dump_layer(layer) return data diff --git a/multi_user/bl_types/bl_image.py b/multi_user/bl_types/bl_image.py index 8b965f1..8497725 100644 --- a/multi_user/bl_types/bl_image.py +++ b/multi_user/bl_types/bl_image.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils import os @@ -10,8 +28,10 @@ def dump_image(image): if image.source == "GENERATED": prefs = utils.get_preferences() img_name = "{}.png".format(image.name) - + + # Cache the image on the disk image.filepath_raw = os.path.join(prefs.cache_directory, img_name) + os.makedirs(prefs.cache_directory, exist_ok=True) image.file_format = "PNG" image.save() @@ -35,14 +55,14 @@ class BlImage(BlDatablock): bl_automatic_push = False bl_icon = 'IMAGE_DATA' - def construct(self, data): + def _construct(self, data): return bpy.data.images.new( name=data['name'], width=data['size'][0], height=data['size'][1] ) - def load(self, data, target): + def _load(self, data, target): image = target prefs = utils.get_preferences() @@ -59,7 +79,7 @@ class BlImage(BlDatablock): image.colorspace_settings.name = data["colorspace_settings"]["name"] - def dump(self, data, pointer=None): + def _dump(self, pointer=None): assert(pointer) data = {} data['pixels'] = dump_image(pointer) diff --git a/multi_user/bl_types/bl_lattice.py b/multi_user/bl_types/bl_lattice.py index 5a7ac28..ca6de99 100644 --- a/multi_user/bl_types/bl_lattice.py +++ b/multi_user/bl_types/bl_lattice.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -18,7 +36,7 @@ class BlLattice(BlDatablock): for point in data['points']: utils.dump_anything.load(target.points[point], data["points"][point]) - def construct(self, data): + def _construct(self, data): return bpy.data.lattices.new(data["name"]) def dump_implementation(self, data, pointer=None): diff --git a/multi_user/bl_types/bl_library.py b/multi_user/bl_types/bl_library.py index 8e909ac..f53e6de 100644 --- a/multi_user/bl_types/bl_library.py +++ b/multi_user/bl_types/bl_library.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -13,11 +31,11 @@ class BlLibrary(BlDatablock): bl_automatic_push = True bl_icon = 'LIBRARY_DATA_DIRECT' - def construct(self, data): + def _construct(self, data): with bpy.data.libraries.load(filepath=data["filepath"], link=True) as (sourceData, targetData): targetData = sourceData return sourceData - def load(self, data, target): + def _load(self, data, target): pass def dump(self, pointer=None): diff --git a/multi_user/bl_types/bl_light.py b/multi_user/bl_types/bl_light.py index 6dfdd80..67f8af9 100644 --- a/multi_user/bl_types/bl_light.py +++ b/multi_user/bl_types/bl_light.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -13,10 +31,10 @@ class BlLight(BlDatablock): bl_automatic_push = True bl_icon = 'LIGHT_DATA' - def construct(self, data): + def _construct(self, data): return bpy.data.lights.new(data["name"], data["type"]) - def load(self, data, target): + def load_implementation(self, data, target): utils.dump_anything.load(target, data) def dump_implementation(self, data, pointer=None): @@ -41,7 +59,8 @@ class BlLight(BlDatablock): "contact_shadow_distance", "contact_shadow_soft_size", "contact_shadow_bias", - "contact_shadow_thickness" + "contact_shadow_thickness", + "shape" ] data = dumper.dump(pointer) return data diff --git a/multi_user/bl_types/bl_lightprobe.py b/multi_user/bl_types/bl_lightprobe.py index bc8edfd..abd20e9 100644 --- a/multi_user/bl_types/bl_lightprobe.py +++ b/multi_user/bl_types/bl_lightprobe.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils import logging @@ -15,16 +33,16 @@ class BlLightprobe(BlDatablock): bl_automatic_push = True bl_icon = 'LIGHTPROBE_GRID' - def load_implementation(self, data, target): - utils.dump_anything.load(target, data) - - def construct(self, data): + def _construct(self, data): type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type'] # See https://developer.blender.org/D6396 if bpy.app.version[1] >= 83: return bpy.data.lightprobes.new(data["name"], type) else: - logger.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396") + logger.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396") + + def load_implementation(self, data, target): + utils.dump_anything.load(target, data) def dump_implementation(self, data, pointer=None): assert(pointer) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index fa33f40..228283d 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils import logging @@ -7,66 +25,62 @@ from ..libs import dump_anything from .bl_datablock import BlDatablock logger = logging.getLogger(__name__) -def clean_color_ramp(target_ramp): - # clear existing - try: - for key in target_ramp.elements: - target_ramp.elements.remove(key) - except: - pass - -def load_mapping(target_apping, source_mapping): - # clear existing curves - for curve in target_apping.curves: - for point in curve.points: - try: - curve.remove(point) - except: - continue - - # Load curves - for curve in source_mapping['curves']: - for point in source_mapping['curves'][curve]['points']: - pos = source_mapping['curves'][curve]['points'][point]['location'] - target_apping.curves[curve].points.new(pos[0],pos[1]) +def load_node(node_data, node_tree): + """ Load a node into a node_tree from a dict -def load_node(target_node_tree, source): - target_node = target_node_tree.nodes.get(source["name"]) + :arg node_data: dumped node data + :type node_data: dict + :arg node_tree: target node_tree + :type node_tree: bpy.types.NodeTree + """ + target_node = node_tree.nodes.new(type=node_data["bl_idname"]) - if target_node is None: - node_type = source["bl_idname"] + dump_anything.load(target_node, node_data) - target_node = target_node_tree.nodes.new(type=node_type) - - # Clean color ramp before loading it - if source['type'] == 'VALTORGB': - clean_color_ramp(target_node.color_ramp) - if source['type'] == 'CURVE_RGB': - load_mapping(target_node.mapping, source['mapping']) - dump_anything.load( - target_node, - source) - - if source['type'] == 'TEX_IMAGE': - target_node.image = bpy.data.images[source['image']] - for input in source["inputs"]: + for input in node_data["inputs"]: if hasattr(target_node.inputs[input], "default_value"): try: - target_node.inputs[input].default_value = source["inputs"][input]["default_value"] + target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"] except: logger.error("{} not supported, skipping".format(input)) -def load_link(target_node_tree, source): - input_socket = target_node_tree.nodes[source['to_node'] - ['name']].inputs[source['to_socket']['name']] - output_socket = target_node_tree.nodes[source['from_node'] - ['name']].outputs[source['from_socket']['name']] +def load_links(links_data, node_tree): + """ Load node_tree links from a list + + :arg links_data: dumped node links + :type links_data: list + :arg node_tree: node links collection + :type node_tree: bpy.types.NodeTree + """ - target_node_tree.links.new(input_socket, output_socket) + for link in links_data: + input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])] + output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])] + node_tree.links.new(input_socket, output_socket) + +def dump_links(links): + """ Dump node_tree links collection to a list + + :arg links: node links collection + :type links: bpy.types.NodeLinks + :retrun: list + """ + + links_data = [] + + for link in links: + links_data.append({ + 'to_node':link.to_node.name, + 'to_socket':link.to_socket.path_from_id()[-2:-1], + 'from_node':link.from_node.name, + 'from_socket':link.from_socket.path_from_id()[-2:-1], + }) + + return links_data class BlMaterial(BlDatablock): bl_id = "materials" @@ -76,7 +90,7 @@ class BlMaterial(BlDatablock): bl_automatic_push = True bl_icon = 'MATERIAL_DATA' - def construct(self, data): + def _construct(self, data): return bpy.data.materials.new(data["name"]) def load_implementation(self, data, target): @@ -100,19 +114,25 @@ class BlMaterial(BlDatablock): # Load nodes for node in data["node_tree"]["nodes"]: - load_node(target.node_tree, data["node_tree"]["nodes"][node]) + load_node(data["node_tree"]["nodes"][node], target.node_tree) # Load nodes links target.node_tree.links.clear() - for link in data["node_tree"]["links"]: - load_link(target.node_tree, data["node_tree"]["links"][link]) + load_links(data["node_tree"]["links"], target.node_tree) def dump_implementation(self, data, pointer=None): assert(pointer) mat_dumper = dump_anything.Dumper() mat_dumper.depth = 2 mat_dumper.exclude_filter = [ + "is_embed_data", + "is_evaluated", + "name_full", + "bl_description", + "bl_icon", + "bl_idname", + "bl_label", "preview", "original", "uuid", @@ -121,47 +141,44 @@ class BlMaterial(BlDatablock): "line_color", "view_center", ] - node_dumper = dump_anything.Dumper() - node_dumper.depth = 1 - node_dumper.exclude_filter = [ - "dimensions", - "show_expanded" - "select", - "bl_height_min", - "bl_height_max", - "bl_width_min", - "bl_width_max", - "bl_width_default", - "hide", - "show_options", - "show_tetxures", - "show_preview", - "outputs", - "width_hidden" - ] - input_dumper = dump_anything.Dumper() - input_dumper.depth = 2 - input_dumper.include_filter = ["default_value"] - links_dumper = dump_anything.Dumper() - links_dumper.depth = 3 - links_dumper.include_filter = [ - "name", - "to_node", - "from_node", - "from_socket", - "to_socket"] data = mat_dumper.dump(pointer) if pointer.use_nodes: nodes = {} - + node_dumper = dump_anything.Dumper() + node_dumper.depth = 1 + node_dumper.exclude_filter = [ + "dimensions", + "show_expanded", + "name_full", + "select", + "bl_height_min", + "bl_height_max", + "bl_width_min", + "bl_width_max", + "type", + "bl_icon", + "bl_width_default", + "bl_static_type", + "show_tetxure", + "hide", + "show_options", + "show_preview", + "outputs", + "width_hidden" + ] for node in pointer.node_tree.nodes: + nodes[node.name] = node_dumper.dump(node) if hasattr(node, 'inputs'): nodes[node.name]['inputs'] = {} - for i in node.inputs: + for i in node.inputs: + input_dumper = dump_anything.Dumper() + input_dumper.depth = 2 + input_dumper.include_filter = ["default_value"] + if hasattr(i, 'default_value'): nodes[node.name]['inputs'][i.name] = input_dumper.dump( i) @@ -184,11 +201,39 @@ class BlMaterial(BlDatablock): 'location' ] nodes[node.name]['mapping'] = curve_dumper.dump(node.mapping) + data["node_tree"]['nodes'] = nodes - data["node_tree"]["links"] = links_dumper.dump(pointer.node_tree.links) + + + data["node_tree"]["links"] = dump_links(pointer.node_tree.links) elif pointer.is_grease_pencil: - data['grease_pencil'] = dump_anything.dump(pointer.grease_pencil, 3) + gp_mat_dumper = dump_anything.Dumper() + gp_mat_dumper.depth = 3 + + gp_mat_dumper.include_filter = [ + 'show_stroke', + 'mode', + 'stroke_style', + 'color', + 'use_overlap_strokes', + 'show_fill', + 'fill_style', + 'fill_color', + 'pass_index', + 'alignment_mode', + # 'fill_image', + 'texture_opacity', + 'mix_factor', + 'texture_offset', + 'texture_angle', + 'texture_scale', + 'texture_clamp', + 'gradient_type', + 'mix_color', + 'flip' + ] + data['grease_pencil'] = gp_mat_dumper.dump(pointer.grease_pencil) return data def resolve_deps_implementation(self): diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index f502cae..51ca774 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -1,163 +1,213 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import bmesh import mathutils +import logging +import numpy as np from .. import utils from ..libs.replication.replication.constants import DIFF_BINARY from .bl_datablock import BlDatablock - -def dump_mesh(mesh, data={}): - import bmesh - - mesh_data = data - mesh_buffer = bmesh.new() - - # https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html - mesh_buffer.from_mesh(mesh) - - uv_layer = mesh_buffer.loops.layers.uv.verify() - bevel_layer = mesh_buffer.verts.layers.bevel_weight.verify() - skin_layer = mesh_buffer.verts.layers.skin.verify() - - verts = {} - for vert in mesh_buffer.verts: - v = {} - v["co"] = list(vert.co) - - # vert metadata - v['bevel'] = vert[bevel_layer] - v['normal'] = list(vert.normal) - # v['skin'] = list(vert[skin_layer]) - - verts[str(vert.index)] = v - - mesh_data["verts"] = verts - - edges = {} - for edge in mesh_buffer.edges: - e = {} - e["verts"] = [edge.verts[0].index, edge.verts[1].index] - - # Edge metadata - e["smooth"] = edge.smooth - - edges[edge.index] = e - mesh_data["edges"] = edges - - faces = {} - for face in mesh_buffer.faces: - f = {} - fverts = [] - for vert in face.verts: - fverts.append(vert.index) - - f["verts"] = fverts - f["material_index"] = face.material_index - f["smooth"] = face.smooth - f["normal"] = list(face.normal) - f["index"] = face.index - - uvs = [] - # Face metadata - for loop in face.loops: - loop_uv = loop[uv_layer] - - uvs.append(list(loop_uv.uv)) - - f["uv"] = uvs - faces[face.index] = f - - mesh_data["faces"] = faces - - uv_layers = [] - for uv_layer in mesh.uv_layers: - uv_layers.append(uv_layer.name) - - mesh_data["uv_layers"] = uv_layers - # return mesh_data +logger = logging.getLogger(__name__) class BlMesh(BlDatablock): bl_id = "meshes" bl_class = bpy.types.Mesh - bl_delay_refresh = 10 - bl_delay_apply = 10 + bl_delay_refresh = 2 + bl_delay_apply = 1 bl_automatic_push = True bl_icon = 'MESH_DATA' - def construct(self, data): + def _construct(self, data): instance = bpy.data.meshes.new(data["name"]) instance.uuid = self.uuid return instance def load_implementation(self, data, target): if not target or not target.is_editmode: - # 1 - LOAD MATERIAL SLOTS - # SLots - i = 0 + utils.dump_anything.load(target, data) + + # MATERIAL SLOTS + target.materials.clear() for m in data["material_list"]: target.materials.append(bpy.data.materials[m]) - # 2 - LOAD GEOMETRY - mesh_buffer = bmesh.new() + # CLEAR GEOMETRY + if target.vertices: + target.clear_geometry() - for i in data["verts"]: - v = mesh_buffer.verts.new(data["verts"][i]["co"]) - v.normal = data["verts"][i]["normal"] - mesh_buffer.verts.ensure_lookup_table() + # VERTS + vertices = np.frombuffer(data["verts_co"], dtype=np.float64) + vert_count = int(len(vertices)/3) + target.vertices.add(vert_count) - for i in data["edges"]: - verts = mesh_buffer.verts - v1 = data["edges"][i]["verts"][0] - v2 = data["edges"][i]["verts"][1] - edge = mesh_buffer.edges.new([verts[v1], verts[v2]]) - edge.smooth = data["edges"][i]["smooth"] + # EDGES + + egdes_vert = np.frombuffer(data["egdes_vert"], dtype=np.int) - mesh_buffer.edges.ensure_lookup_table() - for p in data["faces"]: - verts = [] - for v in data["faces"][p]["verts"]: - verts.append(mesh_buffer.verts[v]) + edge_count = data["egdes_count"] + target.edges.add(edge_count) + + - if len(verts) > 0: - f = mesh_buffer.faces.new(verts) + # LOOPS + loops_count = data["loop_count"] + target.loops.add(loops_count) - uv_layer = mesh_buffer.loops.layers.uv.verify() + loop_vertex_index = np.frombuffer( + data['loop_vertex_index'], dtype=np.int) + loop_normal = np.frombuffer(data['loop_normal'], dtype=np.float64) - f.smooth = data["faces"][p]["smooth"] - f.normal = data["faces"][p]["normal"] - f.index = data["faces"][p]["index"] - f.material_index = data["faces"][p]['material_index'] - # UV loading - for i, loop in enumerate(f.loops): - loop_uv = loop[uv_layer] - loop_uv.uv = data["faces"][p]["uv"][i] - mesh_buffer.faces.ensure_lookup_table() - mesh_buffer.to_mesh(target) + # POLY + poly_count = data["poly_count"] + target.polygons.add(poly_count) - # 3 - LOAD METADATA - # uv's - utils.dump_anything.load(target.uv_layers, data['uv_layers']) + poly_loop_start = np.frombuffer( + data["poly_loop_start"], dtype=np.int) + poly_loop_total = np.frombuffer( + data["poly_loop_total"], dtype=np.int) + poly_smooth = np.frombuffer(data["poly_smooth"], dtype=np.bool) - bevel_layer = mesh_buffer.verts.layers.bevel_weight.verify() - skin_layer = mesh_buffer.verts.layers.skin.verify() + poly_mat = np.frombuffer(data["poly_mat"], dtype=np.int) - utils.dump_anything.load(target, data) + # LOADING + target.vertices.foreach_set('co', vertices) + target.edges.foreach_set("vertices", egdes_vert) + + if data['use_customdata_edge_crease']: + edges_crease = np.frombuffer(data["edges_crease"], dtype=np.float64) + target.edges.foreach_set("crease", edges_crease) + + if data['use_customdata_edge_bevel']: + edges_bevel = np.frombuffer(data["edges_bevel"], dtype=np.float64) + target.edges.foreach_set("bevel_weight", edges_bevel) + + target.loops.foreach_set("vertex_index", loop_vertex_index) + target.loops.foreach_set("normal", loop_normal) + target.polygons.foreach_set("loop_total", poly_loop_total) + target.polygons.foreach_set("loop_start", poly_loop_start) + target.polygons.foreach_set("use_smooth", poly_smooth) + target.polygons.foreach_set("material_index", poly_mat) + + + # UV Layers + for layer in data['uv_layers']: + if layer not in target.uv_layers: + target.uv_layers.new(name=layer) + + uv_buffer = np.frombuffer(data["uv_layers"][layer]['data']) + + target.uv_layers[layer].data.foreach_set('uv', uv_buffer) + + target.validate() + target.update() + def dump_implementation(self, data, pointer=None): assert(pointer) + mesh = pointer + dumper = utils.dump_anything.Dumper() - dumper.depth = 2 + dumper.depth = 1 dumper.include_filter = [ 'name', 'use_auto_smooth', - 'auto_smooth_angle' + 'auto_smooth_angle', + 'use_customdata_edge_bevel', + 'use_customdata_edge_crease' ] - data = dumper.dump(pointer) - dump_mesh(pointer, data) + + data = dumper.dump(mesh) + + # TODO: selective dump + # VERTICES + vert_count = len(mesh.vertices) + + verts_co = np.empty(vert_count*3, dtype=np.float64) + mesh.vertices.foreach_get('co', verts_co) + data["verts_co"] = verts_co.tobytes() + + # EDGES + edge_count = len(mesh.edges) + + edges_vert = np.empty(edge_count*2, dtype=np.int) + mesh.edges.foreach_get('vertices', edges_vert) + data["egdes_vert"] = edges_vert.tobytes() + data["egdes_count"] = len(mesh.edges) + + if mesh.use_customdata_edge_crease: + edges_crease = np.empty(edge_count, dtype=np.float64) + mesh.edges.foreach_get('crease', edges_crease) + data["edges_crease"] = edges_crease.tobytes() + + if mesh.use_customdata_edge_bevel: + edges_bevel = np.empty(edge_count, dtype=np.float64) + mesh.edges.foreach_get('bevel_weight', edges_bevel) + data["edges_bevel"] = edges_bevel.tobytes() + + # POLYGONS + poly_count = len(mesh.polygons) + data["poly_count"] = poly_count + + poly_mat = np.empty(poly_count, dtype=np.int) + mesh.polygons.foreach_get("material_index", poly_mat) + data["poly_mat"] = poly_mat.tobytes() + + poly_loop_start = np.empty(poly_count, dtype=np.int) + mesh.polygons.foreach_get("loop_start", poly_loop_start) + data["poly_loop_start"] = poly_loop_start.tobytes() + + poly_loop_total = np.empty(poly_count, dtype=np.int) + mesh.polygons.foreach_get("loop_total", poly_loop_total) + data["poly_loop_total"] = poly_loop_total.tobytes() + + poly_smooth = np.empty(poly_count, dtype=np.bool) + mesh.polygons.foreach_get("use_smooth", poly_smooth) + data["poly_smooth"] = poly_smooth.tobytes() + + # LOOPS + loop_count = len(mesh.loops) + data["loop_count"] = loop_count + + loop_normal = np.empty(loop_count*3, dtype=np.float64) + mesh.loops.foreach_get("normal", loop_normal) + data["loop_normal"] = loop_normal.tobytes() + + loop_vertex_index = np.empty(loop_count, dtype=np.int) + mesh.loops.foreach_get("vertex_index", loop_vertex_index) + data["loop_vertex_index"] = loop_vertex_index.tobytes() + + # UV Layers + data['uv_layers'] = {} + for layer in mesh.uv_layers: + data['uv_layers'][layer.name] = {} + + uv_layer = np.empty(len(layer.data)*2, dtype=np.float64) + layer.data.foreach_get("uv", uv_layer) + + data['uv_layers'][layer.name]['data'] = uv_layer.tobytes() + # Fix material index m_list = [] for material in pointer.materials: diff --git a/multi_user/bl_types/bl_metaball.py b/multi_user/bl_types/bl_metaball.py index 7dec312..36c998f 100644 --- a/multi_user/bl_types/bl_metaball.py +++ b/multi_user/bl_types/bl_metaball.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -13,7 +31,7 @@ class BlMetaball(BlDatablock): bl_automatic_push = True bl_icon = 'META_BALL' - def construct(self, data): + def _construct(self, data): return bpy.data.metaballs.new(data["name"]) def load(self, data, target): diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 26b5469..2abb6ad 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils import logging @@ -8,22 +26,6 @@ from .bl_datablock import BlDatablock logger = logging.getLogger(__name__) -def load_constraints(target, data): - for local_constraint in target.constraints: - if local_constraint.name not in data: - target.constraints.remove(local_constraint) - - for constraint in data: - target_constraint = target.constraints.get(constraint) - - if not target_constraint: - target_constraint = target.constraints.new( - data[constraint]['type']) - - utils.dump_anything.load( - target_constraint, data[constraint]) - - def load_pose(target_bone, data): target_bone.rotation_mode = data['rotation_mode'] @@ -38,7 +40,7 @@ class BlObject(BlDatablock): bl_automatic_push = True bl_icon = 'OBJECT_DATA' - def construct(self, data): + def _construct(self, data): pointer = None if self.is_library: @@ -50,7 +52,7 @@ class BlObject(BlDatablock): instance.uuid = self.uuid return instance - # Object specific constructor... + # TODO: refactoring if "data" not in data: pass elif data["data"] in bpy.data.meshes.keys(): @@ -87,32 +89,7 @@ class BlObject(BlDatablock): def load_implementation(self, data, target): # Load transformation data - rot_mode = 'rotation_quaternion' if data['rotation_mode'] == 'QUATERNION' else 'rotation_euler' - target.rotation_mode = data['rotation_mode'] - target.location = data['location'] - setattr(target, rot_mode, data[rot_mode]) - target.scale = data['scale'] - - target.name = data["name"] - # Load modifiers - if hasattr(target, 'modifiers'): - # TODO: smarter selective update - target.modifiers.clear() - - for modifier in data['modifiers']: - target_modifier = target.modifiers.get(modifier) - - if not target_modifier: - target_modifier = target.modifiers.new( - data['modifiers'][modifier]['name'], data['modifiers'][modifier]['type']) - - utils.dump_anything.load( - target_modifier, data['modifiers'][modifier]) - - # Load constraints - # Object - if hasattr(target, 'constraints') and 'constraints' in data: - load_constraints(target, data['constraints']) + utils.dump_anything.load(target, data) # Pose if 'pose' in data: @@ -135,28 +112,14 @@ class BlObject(BlDatablock): bone_data = data['pose']['bones'].get(bone) if 'constraints' in bone_data.keys(): - load_constraints( - target_bone, bone_data['constraints']) + utils.dump_anything.load(target_bone, bone_data['constraints']) + load_pose(target_bone, bone_data) if 'bone_index' in bone_data.keys(): target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']] - # Load relations - if 'children' in data.keys(): - for child in data['children']: - bpy.data.objects[child].parent = self.pointer - - # Load empty representation - target.empty_display_size = data['empty_display_size'] - target.empty_display_type = data['empty_display_type'] - - # Instancing - target.instance_type = data['instance_type'] - if data['instance_type'] == 'COLLECTION': - target.instance_collection = bpy.data.collections[data['instance_collection']] - # vertex groups if 'vertex_groups' in data: target.vertex_groups.clear() @@ -220,7 +183,6 @@ class BlObject(BlDatablock): data["modifiers"] = {} for index, modifier in enumerate(pointer.modifiers): data["modifiers"][modifier.name] = dumper.dump(modifier) - data["modifiers"][modifier.name]['m_index'] = index # CONSTRAINTS # OBJECT @@ -299,18 +261,18 @@ class BlObject(BlDatablock): data['vertex_groups'] = vg_data # SHAPE KEYS - pointer_data = pointer.data - if hasattr(pointer_data, 'shape_keys') and pointer_data.shape_keys: + object_data = pointer.data + if hasattr(object_data, 'shape_keys') and object_data.shape_keys: dumper = utils.dump_anything.Dumper() dumper.depth = 2 dumper.include_filter = [ 'reference_key', 'use_relative' ] - data['shape_keys'] = dumper.dump(pointer_data.shape_keys) - data['shape_keys']['reference_key'] = pointer_data.shape_keys.reference_key.name + data['shape_keys'] = dumper.dump(object_data.shape_keys) + data['shape_keys']['reference_key'] = object_data.shape_keys.reference_key.name key_blocks = {} - for key in pointer_data.shape_keys.key_blocks: + for key in object_data.shape_keys.key_blocks: dumper.depth = 3 dumper.include_filter = [ 'name', diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index bac83f8..5b7b9e5 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -12,12 +30,12 @@ class BlScene(BlDatablock): bl_automatic_push = True bl_icon = 'SCENE_DATA' - def construct(self, data): + def _construct(self, data): instance = bpy.data.scenes.new(data["name"]) instance.uuid = self.uuid return instance - def load(self, data, target): + def load_implementation(self, data, target): target = self.pointer # Load other meshes metadata utils.dump_anything.load(target, data) @@ -55,7 +73,13 @@ class BlScene(BlDatablock): scene_dumper = utils.dump_anything.Dumper() scene_dumper.depth = 1 - scene_dumper.include_filter = ['name','world', 'id', 'camera', 'grease_pencil'] + scene_dumper.include_filter = [ + 'name', + 'world', + 'id', + 'camera', + 'grease_pencil' + ] data = scene_dumper.dump(pointer) scene_dumper.depth = 3 diff --git a/multi_user/bl_types/bl_speaker.py b/multi_user/bl_types/bl_speaker.py index d675ee2..9d2f87c 100644 --- a/multi_user/bl_types/bl_speaker.py +++ b/multi_user/bl_types/bl_speaker.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils @@ -16,7 +34,7 @@ class BlSpeaker(BlDatablock): def load_implementation(self, data, target): utils.dump_anything.load(target, data) - def construct(self, data): + def _construct(self, data): return bpy.data.speakers.new(data["name"]) def dump_implementation(self, data, pointer=None): diff --git a/multi_user/bl_types/bl_world.py b/multi_user/bl_types/bl_world.py index 588ef64..8f79dc1 100644 --- a/multi_user/bl_types/bl_world.py +++ b/multi_user/bl_types/bl_world.py @@ -1,9 +1,27 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy import mathutils from .. import utils from .bl_datablock import BlDatablock -from .bl_material import load_link, load_node +from .bl_material import load_links, load_node, dump_links class BlWorld(BlDatablock): @@ -14,10 +32,10 @@ class BlWorld(BlDatablock): bl_automatic_push = True bl_icon = 'WORLD_DATA' - def construct(self, data): + def _construct(self, data): return bpy.data.worlds.new(data["name"]) - def load(self, data, target): + def load_implementation(self, data, target): if data["use_nodes"]: if target.node_tree is None: target.use_nodes = True @@ -25,13 +43,13 @@ class BlWorld(BlDatablock): target.node_tree.nodes.clear() for node in data["node_tree"]["nodes"]: - load_node(target.node_tree, data["node_tree"]["nodes"][node]) + load_node(data["node_tree"]["nodes"][node], target.node_tree) # Load nodes links target.node_tree.links.clear() - for link in data["node_tree"]["links"]: - load_link(target.node_tree, data["node_tree"]["links"][link]) + + load_links(data["node_tree"]["links"], target.node_tree) def dump_implementation(self, data, pointer=None): assert(pointer) @@ -86,8 +104,9 @@ class BlWorld(BlDatablock): nodes[node.name]['inputs'][i.name] = input_dumper.dump( i) data["node_tree"]['nodes'] = nodes - utils.dump_datablock_attibutes( - pointer.node_tree, ["links"], 3, data['node_tree']) + + data["node_tree"]['links'] = dump_links(pointer.node_tree.links) + return data def resolve_deps_implementation(self): @@ -101,6 +120,3 @@ class BlWorld(BlDatablock): deps.append(self.pointer.library) return deps - def is_valid(self): - return bpy.data.worlds.get(self.data['name']) - diff --git a/multi_user/delayable.py b/multi_user/delayable.py index 6c100ab..78598d4 100644 --- a/multi_user/delayable.py +++ b/multi_user/delayable.py @@ -1,3 +1,20 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + import logging import bpy diff --git a/multi_user/environment.py b/multi_user/environment.py index 077f451..716fcf8 100644 --- a/multi_user/environment.py +++ b/multi_user/environment.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import collections import logging import os diff --git a/multi_user/libs/dump_anything.py b/multi_user/libs/dump_anything.py index 9c4f3fc..d686085 100644 --- a/multi_user/libs/dump_anything.py +++ b/multi_user/libs/dump_anything.py @@ -1,7 +1,27 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + +import logging + import bpy import bpy.types as T import mathutils +logger = logging.getLogger(__name__) def remove_items_from_dict(d, keys, recursive=False): copy = dict(d) @@ -47,7 +67,7 @@ def _load_filter_type(t, use_bl_rna=True): if use_bl_rna and x.bl_rna_property: return isinstance(x.bl_rna_property, t) else: - isinstance(x.read(), t) + return isinstance(x.read(), t) return filter_function @@ -73,8 +93,9 @@ def _load_filter_default(default): class Dumper: + # TODO: support occlude readonly def __init__(self): - self.verbose = False + self.verbose = True self.depth = 1 self.keep_compounds_as_leaves = False self.accept_read_only = True @@ -83,7 +104,6 @@ class Dumper: self.type_subset = self.match_subset_all self.include_filter = [] self.exclude_filter = [] - # self._atomic_types = [] # TODO future option? def dump(self, any): return self._dump_any(any, 0) @@ -175,7 +195,8 @@ class Dumper: if (self.include_filter and p not in self.include_filter): return False getattr(default, p) - except AttributeError: + except AttributeError as err: + logger.debug(err) return False if p.startswith("__"): return False @@ -238,14 +259,12 @@ class BlenderAPIElement: def write(self, value): # take precaution if property is read-only - try: - if self.sub_element_name: - setattr(self.api_element, self.sub_element_name, value) - else: - self.api_element = value - except AttributeError as err: - if not self.occlude_read_only: - raise err + if self.sub_element_name and \ + not self.api_element.is_property_readonly(self.sub_element_name): + + setattr(self.api_element, self.sub_element_name, value) + else: + self.api_element = value def extend(self, element_name): return BlenderAPIElement(self.read(), element_name) @@ -262,7 +281,7 @@ class BlenderAPIElement: class Loader: def __init__(self): self.type_subset = self.match_subset_all - self.occlude_read_only = True + self.occlude_read_only = False self.order = ['*'] def load(self, dst_data, src_dumped_data): @@ -287,6 +306,7 @@ class Loader: for i in range(len(dump)): element.read()[i] = dump[i] except AttributeError as err: + logger.debug(err) if not self.occlude_read_only: raise err @@ -297,29 +317,77 @@ class Loader: CONSTRUCTOR_NEW = "new" CONSTRUCTOR_ADD = "add" + DESTRUCTOR_REMOVE = "remove" + DESTRUCTOR_CLEAR = "clear" + constructors = { T.ColorRampElement: (CONSTRUCTOR_NEW, ["position"]), - T.ParticleSettingsTextureSlot: (CONSTRUCTOR_ADD, []) + T.ParticleSettingsTextureSlot: (CONSTRUCTOR_ADD, []), + T.Modifier: (CONSTRUCTOR_NEW, ["name", "type"]), + T.Constraint: (CONSTRUCTOR_NEW, ["type"]), + # T.VertexGroup: (CONSTRUCTOR_NEW, ["name"], True), + } + + destructors = { + T.ColorRampElement:DESTRUCTOR_REMOVE, + T.Modifier: DESTRUCTOR_CLEAR, + T.Constraint: CONSTRUCTOR_NEW, } element_type = element.bl_rna_property.fixed_type + constructor = constructors.get(type(element_type)) + if constructor is None: # collection type not supported return - for dumped_element in dump.values(): - try: - constructor_parameters = [dumped_element[name] - for name in constructor[1]] - except KeyError: - print("Collection load error, missing parameters.") - continue # TODO handle error - new_element = getattr(element.read(), constructor[0])( - *constructor_parameters) + + destructor = destructors.get(type(element_type)) + + # Try to clear existing + if destructor: + if destructor == DESTRUCTOR_REMOVE: + collection = element.read() + for i in range(len(collection)-1): + collection.remove(collection[0]) + else: + getattr(element.read(), DESTRUCTOR_CLEAR)() + + for dump_idx, dumped_element in enumerate(dump.values()): + if dump_idx == 0 and len(element.read())>0: + new_element = element.read()[0] + else: + try: + constructor_parameters = [dumped_element[name] + for name in constructor[1]] + except KeyError: + logger.debug("Collection load error, missing parameters.") + continue # TODO handle error + + new_element = getattr(element.read(), constructor[0])( + *constructor_parameters) self._load_any( BlenderAPIElement( new_element, occlude_read_only=self.occlude_read_only), dumped_element ) + def _load_curve_mapping(self, element, dump): + mapping = element.read() + # cleanup existing curve + for curve in mapping.curves: + for idx in range(len(curve.points)): + if idx == 0: + break + + curve.points.remove(curve.points[1]) + for curve_index, curve in dump['curves'].items(): + for point_idx, point in curve['points'].items(): + pos = point['location'] + + if len(mapping.curves[curve_index].points) == 1: + mapping.curves[curve_index].points[int(point_idx)].location = pos + else: + mapping.curves[curve_index].points.new(pos[0],pos[1]) + def _load_pointer(self, pointer, dump): rna_property_type = pointer.bl_rna_property.fixed_type if not rna_property_type: @@ -336,6 +404,8 @@ class Loader: pointer.write(bpy.data.meshes.get(dump)) elif isinstance(rna_property_type, T.Material): pointer.write(bpy.data.materials.get(dump)) + elif isinstance(rna_property_type, T.Collection): + pointer.write(bpy.data.collections.get(dump)) def _load_matrix(self, matrix, dump): matrix.write(mathutils.Matrix(dump)) @@ -365,11 +435,11 @@ class Loader: for k in self._ordered_keys(dump.keys()): v = dump[k] if not hasattr(default.read(), k): - continue # TODO error handling + logger.debug(f"Load default, skipping {default} : {k}") try: self._load_any(default.extend(k), v) - except: - pass + except Exception as err: + logger.debug(f"Cannot load {k}: {err}") @property def match_subset_all(self): @@ -382,6 +452,7 @@ class Loader: (_load_filter_type(mathutils.Vector, use_bl_rna=False), self._load_vector), (_load_filter_type(mathutils.Quaternion, use_bl_rna=False), self._load_quaternion), (_load_filter_type(mathutils.Euler, use_bl_rna=False), self._load_euler), + (_load_filter_type(T.CurveMapping, use_bl_rna=False), self._load_curve_mapping), (_load_filter_type(T.FloatProperty), self._load_identity), (_load_filter_type(T.StringProperty), self._load_identity), (_load_filter_type(T.EnumProperty), self._load_identity), diff --git a/multi_user/libs/overrider.py b/multi_user/libs/overrider.py deleted file mode 100644 index 964833d..0000000 --- a/multi_user/libs/overrider.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Context Manager allowing temporary override of attributes - -````python -import bpy -from overrider import Overrider - -with Overrider(name='bpy_', parent=bpy) as bpy_: - # set preview render settings - bpy_.context.scene.render.use_file_extension = False - bpy_.context.scene.render.resolution_x = 512 - bpy_.context.scene.render.resolution_y = 512 - bpy_.context.scene.render.use_file_extension = False - bpy_.context.scene.render.image_settings.file_format = "JPEG" - bpy_.context.scene.layers[10] = False - - frame_start = action.frame_range[0] - frame_end = action.frame_range[1] - if begin_frame is not None: - frame_start = begin_frame - if end_frame is not None: - frame_end = end_frame - - # render - window = bpy_.data.window_managers[0].windows[0] - screen = bpy_.data.window_managers[0].windows[0].screen - area = next(area for area in screen.areas if area.type == 'VIEW_3D') - space = next(space for space in area.spaces if space.type == 'VIEW_3D') - - space.viewport_shade = 'MATERIAL' - space.region_3d.view_perspective = 'CAMERA' - - override_context = { - "window": window._real_value_(), - "screen": screen._real_value_() - } - - if frame_start == frame_end: - bpy.context.scene.frame_set(int(frame_start)) - bpy_.context.scene.render.filepath = os.path.join(directory, "icon.jpg") - bpy.ops.render.opengl(override_context, write_still=True) - - else: - for icon_index, frame_number in enumerate(range(int(frame_start), int(frame_end) + 1)): - bpy.context.scene.frame_set(frame_number) - bpy.context.scene.render.filepath = os.path.join(directory, "icon", "{:04d}.jpg".format(icon_index)) - bpy.ops.render.opengl(override_context, write_still=True) -```` -""" -from collections import OrderedDict - - -class OverrideIter: - - def __init__(self, parent): - self.parent = parent - self.index = -1 - - def __next__(self): - self.index += 1 - try: - return self.parent[self.index] - except IndexError as e: - raise StopIteration - - -class OverrideBase: - - def __init__(self, context_manager, name=None, parent=None): - self._name__ = name - self._context_manager_ = context_manager - self._parent_ = parent - self._changed_attributes_ = OrderedDict() - self._changed_items_ = OrderedDict() - self._children_ = list() - self._original_value_ = self._real_value_() - - def __repr__(self): - return "<{}({})>".format(self.__class__.__name__, self._path_) - - @property - def _name_(self): - raise NotImplementedError() - - @property - def _path_(self): - if isinstance(self._parent_, OverrideBase): - return self._parent_._path_ + self._name_ - - return self._name_ - - def _real_value_(self): - raise NotImplementedError() - - def _restore_(self): - for attribute, original_value in reversed(self._changed_attributes_.items()): - setattr(self._real_value_(), attribute, original_value) - - for item, original_value in reversed(self._changed_items_.items()): - self._real_value_()[item] = original_value - - def __getattr__(self, attr): - new_attribute = OverrideAttribute(self._context_manager_, name=attr, parent=self) - self._children_.append(new_attribute) - return new_attribute - - def __getitem__(self, item): - new_item = OverrideItem(self._context_manager_, name=item, parent=self) - self._children_.append(new_item) - return new_item - - def __iter__(self): - return OverrideIter(self) - - def __setattr__(self, attr, value): - if attr in ( - '_name__', - '_context_manager_', - '_parent_', - '_children_', - '_original_value_', - '_changed_attributes_', - '_changed_items_' - ): - self.__dict__[attr] = value - return - - if attr not in self._changed_attributes_.keys(): - self._changed_attributes_[attr] = getattr(self._real_value_(), attr) - self._context_manager_.register_as_changed(self) - - setattr(self._real_value_(), attr, value) - - def __setitem__(self, item, value): - if item not in self._changed_items_.keys(): - self._changed_items_[item] = self._real_value_()[item] - self._context_manager_.register_as_changed(self) - - self._real_value_()[item] = value - - def __eq__(self, other): - return self._real_value_() == other - - def __gt__(self, other): - return self._real_value_() > other - - def __lt__(self, other): - return self._real_value_() < other - - def __ge__(self, other): - return self._real_value_() >= other - - def __le__(self, other): - return self._real_value_() <= other - - def __call__(self, *args, **kwargs): - # TODO : surround str value with quotes - arguments = list([str(arg) for arg in args]) + ['{}={}'.format(key, value) for key, value in kwargs.items()] - arguments = ', '.join(arguments) - raise RuntimeError('Overrider does not allow call to {}({})'.format(self._path_, arguments)) - - -class OverrideRoot(OverrideBase): - - @property - def _name_(self): - return self._name__ - - def _real_value_(self): - return self._parent_ - - -class OverrideAttribute(OverrideBase): - - @property - def _name_(self): - return '.{}'.format(self._name__) - - def _real_value_(self): - return getattr(self._parent_._real_value_(), self._name__) - - -class OverrideItem(OverrideBase): - - @property - def _name_(self): - if isinstance(self._name__, str): - return '["{}"]'.format(self._name__) - - return '[{}]'.format(self._name__) - - def _real_value_(self): - return self._parent_._real_value_()[self._name__] - - -class Overrider: - def __init__(self, name, parent): - self.name = name - self.parent = parent - self.override = None - self.registered_overrides = list() - - def __enter__(self): - self.override = OverrideRoot( - context_manager=self, - parent=self.parent, - name=self.name - ) - return self.override - - def __exit__(self, exc_type, exc_val, exc_tb): - self.restore() - - def register_as_changed(self, override): - self.registered_overrides.append(override) - - def restore(self): - for override in reversed(self.registered_overrides): - override._restore_() diff --git a/multi_user/libs/replication b/multi_user/libs/replication index 114b2eb..70b2d24 160000 --- a/multi_user/libs/replication +++ b/multi_user/libs/replication @@ -1 +1 @@ -Subproject commit 114b2ebe37ccd10b36544924d23443df0867581b +Subproject commit 70b2d24d15690540c0e0bee43bd82bf338f986c9 diff --git a/multi_user/operators.py b/multi_user/operators.py index 424418f..1d302c1 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import asyncio import logging import os @@ -470,7 +488,7 @@ def sanitize_deps_graph(dummy): if client and client.state['STATE'] in [STATE_ACTIVE]: for node_key in client.list(): - client.get(node_key).resolve() + client.get(node_key)._resolve() @persistent diff --git a/multi_user/preferences.py b/multi_user/preferences.py index e329e1d..29f892c 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import logging import bpy diff --git a/multi_user/presence.py b/multi_user/presence.py index 5494e19..975193d 100644 --- a/multi_user/presence.py +++ b/multi_user/presence.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import copy import logging import math diff --git a/multi_user/ui.py b/multi_user/ui.py index 21f4d48..26f5c8d 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -1,3 +1,21 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import bpy from . import operators, utils diff --git a/multi_user/utils.py b/multi_user/utils.py index db96e92..cd69456 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -1,9 +1,28 @@ +# ##### 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 3 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, see . +# +# ##### END GPL LICENSE BLOCK ##### + + import json import logging import os import random import string import sys +import time from uuid import uuid4 from collections.abc import Iterable @@ -157,4 +176,7 @@ def resolve_from_id(id, optionnal_type=None): def get_preferences(): - return bpy.context.preferences.addons[__package__].preferences \ No newline at end of file + return bpy.context.preferences.addons[__package__].preferences + +def current_milli_time(): + return int(round(time.time() * 1000)) \ No newline at end of file